diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d40bf31..21e36b23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,10 +41,10 @@ jobs: bun-version: "1.3.14" - name: Install dependencies - run: uv sync --all-extras + run: uv sync --all-extras --python ${{ matrix.python-version }} - name: Run tests - run: uv run pytest + run: uv run --python ${{ matrix.python-version }} pytest lint: name: Lint diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 4628f728..a223d61b 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -27,7 +27,8 @@ concurrency: env: # The test suite and native Actions artifact must come from this exact ref. # The downloader verifies its provenance against the checked-out commit. - VGI_REF: fc3e08f7e7fd800165a0edc00313de46fed15329 + VGI_REF: 7b1e1fbbe745d77fdacf52d6d135fa3ef4491d48 + VGI_NATIVE_RUNNER_BRANCH: release/vgi-v3-ci-20261028 # CI is not a user: its ATTACHes should not land in Query.Farm's adoption data. # The vgi extension fires a telemetry event per successful ATTACH, and this # suite performs several hundred of them per lane, so opting out keeps the diff --git a/conformance/catalog-contracts.json b/conformance/catalog-contracts.json new file mode 100644 index 00000000..71cb65dc --- /dev/null +++ b/conformance/catalog-contracts.json @@ -0,0 +1,187 @@ +{ + "format_version": 1, + "generator": "vgi.codegen.catalog_contract_fixtures", + "records": { + "view_info": "/////yADAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAgAAADEAgAA+AEAAMwBAABwAQAAQAEAAIwAAAA0AAAABAAAAKD9//8AAAAFEAAAABwAAAAEAAAAAAAAAAgAAABzZWN1cml0eQAAAABg/f//zP3//wAAAAwUAAAAHAAAAAQAAAABAAAAFAAAAAcAAABhbGlhc2VzAIz9///E/f//AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC0/f//IP7//wAAABEUAAAAJAAAAAQAAAABAAAAHAAAAA8AAABjb2x1bW5fY29tbWVudHMA6P3//1T+//8AAAANGAAAACAAAAAEAAAAAgAAAEAAAAAUAAAABwAAAGVudHJpZXMAGP7//1D+//8AAAEFEAAAABgAAAAEAAAAAAAAAAUAAAB2YWx1ZQAAAED+//+s/v//AAAABRAAAAAUAAAABAAAAAAAAAADAAAAa2V5AGT+///Q/v//AAAABRAAAAAcAAAABAAAAAAAAAAKAAAAZGVmaW5pdGlvbgAAkP7///z+//8AAAAMFAAAACAAAAAEAAAAAQAAABgAAAALAAAAc2NoZW1hX3BhdGgAwP7///j+//8AAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAOj+//9U////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAAAQ////fP///wAAABEUAAAAHAAAAAQAAAABAAAAFAAAAAQAAAB0YWdzAAAAADz///+o////AAAADRgAAAAgAAAABAAAAAIAAABQAAAAFAAAAAcAAABlbnRyaWVzAGz///+k////AAABBRAAAAAYAAAABAAAAAAAAAAFAAAAdmFsdWUAAACU////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAFEAAAABQAAAAEAAAAAAAAAAMAAABrZXkAyP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAcAAAABAAAAAAAAAAHAAAAY29tbWVudAAEAAQABAAAAP/////YAwAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAAQAQAAAAAAAAAACgAYAAwABAAIAAoAAACcAgAAEAAAAAEAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAADAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAIAAAAAAAAACgAAAAAAAAABAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAIAAAAAAAAADgAAAAAAAAAAwAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAIAAAAAAAAAEgAAAAAAAAADQAAAAAAAABYAAAAAAAAAAAAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAwAAAAAAAAAcAAAAAAAAAANAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAgAAAAAAAAAiAAAAAAAAAAhAAAAAAAAALAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAgAAAAAAAAAuAAAAAAAAAAAAAAAAAAAALgAAAAAAAAAAAAAAAAAAAC4AAAAAAAAAAgAAAAAAAAAwAAAAAAAAAADAAAAAAAAAMgAAAAAAAAAAAAAAAAAAADIAAAAAAAAAAgAAAAAAAAA0AAAAAAAAAAEAAAAAAAAANgAAAAAAAAAAAAAAAAAAADYAAAAAAAAAAgAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAADAAAAAAAAADwAAAAAAAAAA0AAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAACAAAAAAAAAAIAQAAAAAAAAcAAAAAAAAAAAAAABAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAB2aWV3IGNvbW1lbnQAAAAAAAAAAAEAAAAAAAAABAAAAHRlYW0AAAAAAAAAAAMAAABvbmUAAAAAAAAAAAANAAAAdmlldy53aXRoLmRvdAAAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAAIQAAAFNFTEVDVCAxIEFTIG9yaWdpbmFsLCAyIEFTIHNlY29uZAAAAAAAAAAAAAAAAQAAAAAAAAADAAAAYS5iAAAAAAAAAAAABAAAAGtlcHQAAAAAAAAAAAIAAAAAAAAAAwAAAA0AAAAAAAAAYS5id2l0aCJxdW90ZQAAAAAAAAAHAAAAcmVndWxhcgD/////AAAAAA==", + "view_info_barrier": "/////yADAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAgAAADEAgAA+AEAAMwBAABwAQAAQAEAAIwAAAA0AAAABAAAAKD9//8AAAAFEAAAABwAAAAEAAAAAAAAAAgAAABzZWN1cml0eQAAAABg/f//zP3//wAAAAwUAAAAHAAAAAQAAAABAAAAFAAAAAcAAABhbGlhc2VzAIz9///E/f//AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC0/f//IP7//wAAABEUAAAAJAAAAAQAAAABAAAAHAAAAA8AAABjb2x1bW5fY29tbWVudHMA6P3//1T+//8AAAANGAAAACAAAAAEAAAAAgAAAEAAAAAUAAAABwAAAGVudHJpZXMAGP7//1D+//8AAAEFEAAAABgAAAAEAAAAAAAAAAUAAAB2YWx1ZQAAAED+//+s/v//AAAABRAAAAAUAAAABAAAAAAAAAADAAAAa2V5AGT+///Q/v//AAAABRAAAAAcAAAABAAAAAAAAAAKAAAAZGVmaW5pdGlvbgAAkP7///z+//8AAAAMFAAAACAAAAAEAAAAAQAAABgAAAALAAAAc2NoZW1hX3BhdGgAwP7///j+//8AAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAOj+//9U////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAAAQ////fP///wAAABEUAAAAHAAAAAQAAAABAAAAFAAAAAQAAAB0YWdzAAAAADz///+o////AAAADRgAAAAgAAAABAAAAAIAAABQAAAAFAAAAAcAAABlbnRyaWVzAGz///+k////AAABBRAAAAAYAAAABAAAAAAAAAAFAAAAdmFsdWUAAACU////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAFEAAAABQAAAAEAAAAAAAAAAMAAABrZXkAyP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAcAAAABAAAAAAAAAAHAAAAY29tbWVudAAEAAQABAAAAP/////YAwAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACoAAAAAAAAAAAACgAYAAwABAAIAAoAAACcAgAAEAAAAAEAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAEAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAIAAAAAAAAADAAAAAAAAAABwAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAgAAAAAAAAASAAAAAAAAAAEAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAUAAAAAAAAAHAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAAgAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAQAAAAAAAAAiAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAAAAAAAAAACIAAAAAAAAAAgAAAAAAAAAkAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAABAAAAAAAAACYAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAAAAAAAAAAAJgAAAAAAAAACAAAAAAAAACgAAAAAAAAAAcAAAAAAAAAAAAAABAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAYmFycmllcgAAAAAAAQAAAAAAAAAEAAAAbWFpbgAAAAAAAAAAFAAAAFNFTEVDVCAxIEFTIG9yaWdpbmFsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAGJhcnJpZXIA/////wAAAAA=", + "view_info_implicit": "/////yADAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAgAAADEAgAA+AEAAMwBAABwAQAAQAEAAIwAAAA0AAAABAAAAKD9//8AAAAFEAAAABwAAAAEAAAAAAAAAAgAAABzZWN1cml0eQAAAABg/f//zP3//wAAAAwUAAAAHAAAAAQAAAABAAAAFAAAAAcAAABhbGlhc2VzAIz9///E/f//AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC0/f//IP7//wAAABEUAAAAJAAAAAQAAAABAAAAHAAAAA8AAABjb2x1bW5fY29tbWVudHMA6P3//1T+//8AAAANGAAAACAAAAAEAAAAAgAAAEAAAAAUAAAABwAAAGVudHJpZXMAGP7//1D+//8AAAEFEAAAABgAAAAEAAAAAAAAAAUAAAB2YWx1ZQAAAED+//+s/v//AAAABRAAAAAUAAAABAAAAAAAAAADAAAAa2V5AGT+///Q/v//AAAABRAAAAAcAAAABAAAAAAAAAAKAAAAZGVmaW5pdGlvbgAAkP7///z+//8AAAAMFAAAACAAAAAEAAAAAQAAABgAAAALAAAAc2NoZW1hX3BhdGgAwP7///j+//8AAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAOj+//9U////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAAAQ////fP///wAAABEUAAAAHAAAAAQAAAABAAAAFAAAAAQAAAB0YWdzAAAAADz///+o////AAAADRgAAAAgAAAABAAAAAIAAABQAAAAFAAAAAcAAABlbnRyaWVzAGz///+k////AAABBRAAAAAYAAAABAAAAAAAAAAFAAAAdmFsdWUAAACU////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAFEAAAABQAAAAEAAAAAAAAAAMAAABrZXkAyP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAcAAAABAAAAAAAAAAHAAAAY29tbWVudAAEAAQABAAAAP/////YAwAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACoAAAAAAAAAAAACgAYAAwABAAIAAoAAACcAgAAEAAAAAEAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAEAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAIAAAAAAAAADAAAAAAAAAACAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAgAAAAAAAAASAAAAAAAAAAEAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAUAAAAAAAAAHAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAAgAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAQAAAAAAAAAiAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAAAAAAAAAACIAAAAAAAAAAgAAAAAAAAAkAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAABAAAAAAAAACYAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAAAAAAAAAAAJgAAAAAAAAACAAAAAAAAACgAAAAAAAAAAcAAAAAAAAAAAAAABAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAaW1wbGljaXQAAAAAAQAAAAAAAAAEAAAAbWFpbgAAAAAAAAAAFAAAAFNFTEVDVCAxIEFTIG9yaWdpbmFsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAHJlZ3VsYXIA/////wAAAAA=", + "capabilities": "/////7gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAAAcAQAAuAAAAGAAAAAEAAAABP///wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAAB0cmFuc2FjdGlvbgDA/v//+P7//wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAA6P7//1z///8AAAAMFAAAABwAAAAEAAAAAQAAABQAAAAFAAAAd3JpdGUAAAAU////TP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAPP///7D///8AAAAMFAAAABgAAAAEAAAAAQAAABAAAAADAAAAZGRsAGT///+c////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAACM////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAMFAAAABwAAAAEAAAAAQAAACQAAAAEAAAAcmVhZAAAAADI////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABwAAAAEAAAAAAAAAAQAAABpdGVtAAAAAAQABAAEAAAAAAAAAP////8YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACAAAAAAAAAAAAACgAYAAwABAAIAAoAAABcAQAAEAAAAAEAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAaAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAgAAAAAAAAAOAAAAAAAAAAAAAAAAAAAADgAAAAAAAAACAAAAAAAAABAAAAAAAAAABwAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAACAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAaAAAAAAAAAAEAAAAAAAAAHAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAIAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAaAAAAdmdpLnNjaGVtYS5vcHRpb25zLnJlYWQudjEAAAAAAAAAAAAAAQAAAAAAAAAcAAAAdmdpLnNjaGVtYS5vcHRpb25zLmNyZWF0ZS52MQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "empty_capabilities": "/////7gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAAAcAQAAuAAAAGAAAAAEAAAABP///wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAAB0cmFuc2FjdGlvbgDA/v//+P7//wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAA6P7//1z///8AAAAMFAAAABwAAAAEAAAAAQAAABQAAAAFAAAAd3JpdGUAAAAU////TP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAPP///7D///8AAAAMFAAAABgAAAAEAAAAAQAAABAAAAADAAAAZGRsAGT///+c////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAACM////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAMFAAAABwAAAAEAAAAAQAAACQAAAAEAAAAcmVhZAAAAADI////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABwAAAAEAAAAAAAAAAQAAABpdGVtAAAAAAQABAAEAAAAAAAAAP////8YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAABAAAAAAAAAAAAACgAYAAwABAAIAAoAAABcAQAAEAAAAAEAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAABAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAACAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAEAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAIAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAQAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "definitions": "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", + "values": "/////+gBAAAQAAAAAAAKAA4ABgAFAAgACgAAAAABBAAQAAAAAAAKAAwAAAAEAAgACgAAAEgAAAAEAAAAAQAAAAQAAACg/v//JAAAAAQAAAAUAAAAY2F0YWxvZy1jb250cmFjdHMvdjEAAAAACAAAAHByb3ZpZGVyAAAAAAIAAADwAAAABAAAACr///8AAAENGAAAAFQAAAAIAAAAGAAAAAEAAABcAAAABgAAAHJlY29yZAAAAQAAAAQAAAAY////HAAAAAQAAAAOAAAAZXhhbXBsZS5yZWNvcmQAAAcAAABsb2dpY2FsAJT///8AABIAGAAIAAAABwAMAAAAEAAUABIAAAAAAAAFFAAAAEwAAAAIAAAAEAAAAAAAAAADAAAAYS5iAAEAAAAEAAAAiP///xgAAAAEAAAACAAAAHJldGFpbmVkAAAAAAYAAABuZXN0ZWQAAAQABgAEAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABoAAAACAAAABgAAAAAAAAACAAAAG9wdGlvbmFsAAAAAAEAAAAMAAAACAAMAAQACAAIAAAAIAAAAAQAAAAQAAAAZXhhbXBsZS5vcHRpb25hbAAAAAAHAAAAbG9naWNhbAAIAAwACAAHAAgAAAAAAAABIAAAAP/////oAAAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAAoAAAAAAAAAAAACgAYAAwABAAIAAoAAAB8AAAAEAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAEAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAADQAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAGxpdGVyYWwuIm5hbWUAAAD/////AAAAAA==", + "empty_values": "/////3gAAAAQAAAAAAAKAA4ABgAFAAgACgAAAAABBAAQAAAAAAAKAAwAAAAEAAgACgAAAEQAAAAEAAAAAQAAAAwAAAAIAAwABAAIAAgAAAAYAAAABAAAAAoAAABuby1vcHRpb25zAAAIAAAAcHJvdmlkZXIAAAAAAAAAAAAAAAD/////SAAAABQAAAAAAAAAAAAKAA4ABgAFAAgACgAAAAADBAAQAAAAAAAKABQADAAEAAgACgAAABQAAAAMAAAAAQAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "metadata_patch": "/////8ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAABcAQAAIAEAAGAAAAAEAAAAxP7//wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAAByZW1vdmVfdGFncwC4/v//LP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAA4P7//xz///8AAAARFAAAACAAAAAEAAAAAQAAABgAAAAIAAAAc2V0X3RhZ3MAAAAAEP///0z///8AAAANGAAAACAAAAAEAAAAAgAAAEAAAAAUAAAABwAAAGVudHJpZXMAQP///7T///8AAAEFEAAAABgAAAAEAAAAAAAAAAUAAAB2YWx1ZQAAAGj///+k////AAAABRAAAAAUAAAABAAAAAAAAAADAAAAa2V5AIz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABwAAAGNvbW1lbnQAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABhAAAAAgAAAABAAAAAAAAAALAAAAc2V0X2NvbW1lbnQABAAEAAQAAAAAAAAA/////wgCAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAHgAAAAAAAAAAAAKABgADAAEAAgACgAAAEwBAAAQAAAAAQAAAAAAAAAAAAAAEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAEAAAAAAAAAAoAAAAAAAAAAwAAAAAAAAAOAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAEAAAAAAAAABIAAAAAAAAABAAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAACAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAMAAAAAAAAAHAAAAAAAAAABgAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAMAAAAMAAAADAAAAGEuYl9fcHJvdG9fXwAAAAAAAAAAAAAAAAcAAAAQAAAAbGl0ZXJhbGVtcHR5LWtleQAAAAACAAAAAAAAAAMAAAAGAAAAAAAAAG9sZE9MRAAA/////wAAAAA=", + "metadata_clear_comment": "/////8ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAABcAQAAIAEAAGAAAAAEAAAAxP7//wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAAByZW1vdmVfdGFncwC4/v//LP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAA4P7//xz///8AAAARFAAAACAAAAAEAAAAAQAAABgAAAAIAAAAc2V0X3RhZ3MAAAAAEP///0z///8AAAANGAAAACAAAAAEAAAAAgAAAEAAAAAUAAAABwAAAGVudHJpZXMAQP///7T///8AAAEFEAAAABgAAAAEAAAAAAAAAAUAAAB2YWx1ZQAAAGj///+k////AAAABRAAAAAUAAAABAAAAAAAAAADAAAAa2V5AIz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABwAAAGNvbW1lbnQAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABhAAAAAgAAAABAAAAAAAAAALAAAAc2V0X2NvbW1lbnQABAAEAAQAAAAAAAAA/////wgCAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAEAAAAAAAAAAAAAKABgADAAEAAgACgAAAEwBAAAQAAAAAQAAAAAAAAAAAAAAEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAABAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAACAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAABAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAABAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAACAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAEAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "metadata_noop": "/////8ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAABcAQAAIAEAAGAAAAAEAAAAxP7//wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAAByZW1vdmVfdGFncwC4/v//LP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAA4P7//xz///8AAAARFAAAACAAAAAEAAAAAQAAABgAAAAIAAAAc2V0X3RhZ3MAAAAAEP///0z///8AAAANGAAAACAAAAAEAAAAAgAAAEAAAAAUAAAABwAAAGVudHJpZXMAQP///7T///8AAAEFEAAAABgAAAAEAAAAAAAAAAUAAAB2YWx1ZQAAAGj///+k////AAAABRAAAAAUAAAABAAAAAAAAAADAAAAa2V5AIz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABwAAAGNvbW1lbnQAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABhAAAAAgAAAABAAAAAAAAAALAAAAc2V0X2NvbW1lbnQABAAEAAQAAAAAAAAA/////wgCAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAEAAAAAAAAAAAAAKABgADAAEAAgACgAAAEwBAAAQAAAAAQAAAAAAAAAAAAAAEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAABAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAACAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAABAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAABAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAACAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAEAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "changes": "/////+AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACAAAAAFAAAABAAFAAIAAAABwAMAAAAEAAQAAAAAAAADBQAAAAgAAAABAAAAAEAAAAYAAAACwAAAHJlc2V0X25hbWVzAJz////Y////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAADE////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEEEAAAACAAAAAEAAAAAAAAAAoAAABzZXRfdmFsdWVzAAAEAAQABAAAAP////8IAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAAwAwAAAAAAAAAACgAYAAwABAAIAAoAAACcAAAAEAAAAAEAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAMAAAAAAAAYAwAAAAAAAAAAAAAAAAAAGAMAAAAAAAAIAAAAAAAAACADAAAAAAAAAAAAAAAAAAAgAwAAAAAAAAgAAAAAAAAAKAMAAAAAAAAIAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAABADAAD/////6AEAABAAAAAAAAoADgAGAAUACAAKAAAAAAEEABAAAAAAAAoADAAAAAQACAAKAAAASAAAAAQAAAABAAAABAAAAKD+//8kAAAABAAAABQAAABjYXRhbG9nLWNvbnRyYWN0cy92MQAAAAAIAAAAcHJvdmlkZXIAAAAAAgAAAPAAAAAEAAAAKv///wAAAQ0YAAAAVAAAAAgAAAAYAAAAAQAAAFwAAAAGAAAAcmVjb3JkAAABAAAABAAAABj///8cAAAABAAAAA4AAABleGFtcGxlLnJlY29yZAAABwAAAGxvZ2ljYWwAlP///wAAEgAYAAgAAAAHAAwAAAAQABQAEgAAAAAAAAUUAAAATAAAAAgAAAAQAAAAAAAAAAMAAABhLmIAAQAAAAQAAACI////GAAAAAQAAAAIAAAAcmV0YWluZWQAAAAABgAAAG5lc3RlZAAABAAGAAQAAAAAABIAGAAIAAYABwAMAAAAEAAUABIAAAAAAAECFAAAAGgAAAAIAAAAGAAAAAAAAAAIAAAAb3B0aW9uYWwAAAAAAQAAAAwAAAAIAAwABAAIAAgAAAAgAAAABAAAABAAAABleGFtcGxlLm9wdGlvbmFsAAAAAAcAAABsb2dpY2FsAAgADAAIAAcACAAAAAAAAAEgAAAA/////+gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAACgAAAAAAAAAAAAKABgADAAEAAgACgAAAHwAAAAQAAAAAQAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAANAAAAAAAAAAAAAAADAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAAAAbGl0ZXJhbC4ibmFtZQAAAP////8AAAAAAAAAAAEAAAAAAAAACAAAAHJlc2V0Lm1l/////wAAAAA=", + "reset_only": "/////+AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACAAAAAFAAAABAAFAAIAAAABwAMAAAAEAAQAAAAAAAADBQAAAAgAAAABAAAAAEAAAAYAAAACwAAAHJlc2V0X25hbWVzAJz////Y////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAADE////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEEEAAAACAAAAAEAAAAAAAAAAoAAABzZXRfdmFsdWVzAAAEAAQABAAAAP////8IAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAAoAAAAAAAAAAAACgAYAAwABAAIAAoAAACcAAAAEAAAAAEAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAAIAAAAAAAAAAAAAAADAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAgAAAByZXNldC5tZf////8AAAAA", + "state": "/////+AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACEAAAABAAAAJT///8AAAAMFAAAACQAAAAEAAAAAQAAACwAAAAOAAAAZXhwbGljaXRfbmFtZXMAAJD///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAyP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAcAAAABAAAAAAAAAAGAAAAdmFsdWVzAAAEAAQABAAAAP////8IAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAAwAwAAAAAAAAAACgAYAAwABAAIAAoAAACcAAAAEAAAAAEAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAMAAAAAAAAYAwAAAAAAAAAAAAAAAAAAGAMAAAAAAAAIAAAAAAAAACADAAAAAAAAAAAAAAAAAAAgAwAAAAAAAAgAAAAAAAAAKAMAAAAAAAAIAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAABADAAD/////6AEAABAAAAAAAAoADgAGAAUACAAKAAAAAAEEABAAAAAAAAoADAAAAAQACAAKAAAASAAAAAQAAAABAAAABAAAAKD+//8kAAAABAAAABQAAABjYXRhbG9nLWNvbnRyYWN0cy92MQAAAAAIAAAAcHJvdmlkZXIAAAAAAgAAAPAAAAAEAAAAKv///wAAAQ0YAAAAVAAAAAgAAAAYAAAAAQAAAFwAAAAGAAAAcmVjb3JkAAABAAAABAAAABj///8cAAAABAAAAA4AAABleGFtcGxlLnJlY29yZAAABwAAAGxvZ2ljYWwAlP///wAAEgAYAAgAAAAHAAwAAAAQABQAEgAAAAAAAAUUAAAATAAAAAgAAAAQAAAAAAAAAAMAAABhLmIAAQAAAAQAAACI////GAAAAAQAAAAIAAAAcmV0YWluZWQAAAAABgAAAG5lc3RlZAAABAAGAAQAAAAAABIAGAAIAAYABwAMAAAAEAAUABIAAAAAAAECFAAAAGgAAAAIAAAAGAAAAAAAAAAIAAAAb3B0aW9uYWwAAAAAAQAAAAwAAAAIAAwABAAIAAgAAAAgAAAABAAAABAAAABleGFtcGxlLm9wdGlvbmFsAAAAAAcAAABsb2dpY2FsAAgADAAIAAcACAAAAAAAAAEgAAAA/////+gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAACgAAAAAAAAAAAAKABgADAAEAAgACgAAAHwAAAAQAAAAAQAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAANAAAAAAAAAAAAAAADAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAAAAbGl0ZXJhbC4ibmFtZQAAAP////8AAAAAAAAAAAEAAAAAAAAACAAAAG9wdGlvbmFs/////wAAAAA=", + "empty_state": "/////+AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACEAAAABAAAAJT///8AAAAMFAAAACQAAAAEAAAAAQAAACwAAAAOAAAAZXhwbGljaXRfbmFtZXMAAJD///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAyP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAcAAAABAAAAAAAAAAGAAAAdmFsdWVzAAAEAAQABAAAAP////8IAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAADwAAAAAAAAAAAACgAYAAwABAAIAAoAAACcAAAAEAAAAAEAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA2AAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAIAAAAAAAAAOgAAAAAAAAAAAAAAAAAAADoAAAAAAAAAAQAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAAD/////eAAAABAAAAAAAAoADgAGAAUACAAKAAAAAAEEABAAAAAAAAoADAAAAAQACAAKAAAARAAAAAQAAAABAAAADAAAAAgADAAEAAgACAAAABgAAAAEAAAACgAAAG5vLW9wdGlvbnMAAAgAAABwcm92aWRlcgAAAAAAAAAAAAAAAP////9IAAAAFAAAAAAAAAAAAAoADgAGAAUACAAKAAAAAAMEABAAAAAAAAoAFAAMAAQACAAKAAAAFAAAAAwAAAABAAAAAAAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "paths": "/////wgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgAAAAHAAwAAAAQABAAAAAAAAAMFAAAABwAAAAEAAAAAQAAABQAAAAGAAAAZ3JvdXBzAABw////qP///wAAAQwUAAAAHAAAAAQAAAABAAAAFAAAAAQAAABpdGVtAAAAAJz////U////AAABDBQAAAAcAAAABAAAAAEAAAAkAAAABAAAAGl0ZW0AAAAAyP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAcAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAEAAQABAAAAAAAAAD/////KAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAWAAAAAAAAAAAAAoAGAAMAAQACAAKAAAArAAAABAAAAABAAAAAAAAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAMAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAGAAAAAAAAABAAAAAAAAAABgAAAAAAAAAAAAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAEAAAADAAAABQAAAAAAAAADAAAABAAAAAUAAAALAAAAGAAAAGEuYmFicmVjb3JkbGl0ZXJhbC4ibmFtZf////8AAAAA", + "empty_paths": "/////wgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgAAAAHAAwAAAAQABAAAAAAAAAMFAAAABwAAAAEAAAAAQAAABQAAAAGAAAAZ3JvdXBzAABw////qP///wAAAQwUAAAAHAAAAAQAAAABAAAAFAAAAAQAAABpdGVtAAAAAJz////U////AAABDBQAAAAcAAAABAAAAAEAAAAkAAAABAAAAGl0ZW0AAAAAyP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAcAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAEAAQABAAAAAAAAAD/////KAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAIAAAAAAAAAAAAAoAGAAMAAQACAAKAAAArAAAABAAAAABAAAAAAAAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAABAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "expression": "/////0gCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADwAQAAtAEAAEgBAAAEAAAAMP7//wAAAA0kAAAAOAAAAAQAAAAFAAAA/AAAAMwAAACYAAAAYAAAACAAAAASAAAAZXZhbHVhdGlvbl9jb250ZXh0AABE/v//eP7//wAAAAYQAAAALAAAAAQAAAAAAAAAGAAAAHByZXNlcnZlX2luc2VydGlvbl9vcmRlcgAAAACA/v//tP7//wAAAAUQAAAAJAAAAAQAAAAAAAAAEgAAAGRlZmF1bHRfbnVsbF9vcmRlcgAAtP7//+j+//8AAAAFEAAAACAAAAAEAAAAAAAAAA0AAABkZWZhdWx0X29yZGVyAAAA5P7//xj///8AAAAFEAAAABwAAAAEAAAAAAAAAAkAAABjb2xsYXRpb24AAAAQ////RP///wAAAAUQAAAAHAAAAAQAAAAAAAAACAAAAHRpbWV6b25lAAAAADz///9w////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAGz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAApP///9j///8AAAAFEAAAABgAAAAEAAAAAAAAAAcAAABwcm9maWxlAMz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAGAAAAAQAAAAAAAAAAwAAAHNxbAAEAAQABAAAAP////+YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAC4AAAAAAAAAAAACgAYAAwABAAIAAoAAAC8AQAAEAAAAAEAAAAAAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAACQAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAHwAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAIAAAAAAAAAEgAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAwAAAAAAAAAWAAAAAAAAAANAAAAAAAAAGgAAAAAAAAAAAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAaAAAAAAAAAAIAAAAAAAAAHAAAAAAAAAAAwAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAIAAAAAAAAAIAAAAAAAAAABgAAAAAAAACIAAAAAAAAAAAAAAAAAAAAiAAAAAAAAAAIAAAAAAAAAJAAAAAAAAAAAwAAAAAAAACYAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAIAAAAAAAAAKAAAAAAAAAACgAAAAAAAACwAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAABAAAAAAAAAAAAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAImEuYiIgKyAxAAAAAAAAAAAAAAAfAAAAdmdpLmV4cHJlc3Npb24uZHVja2RiLnNjYWxhci52MQAAAAAAAgAAAAAAAAAHAAAADQAAAAAAAABvbmUudHdvbmVzdGVkAAAAAAAAAAMAAABVVEMAAAAAAAAAAAAGAAAAYmluYXJ5AAAAAAAAAwAAAGFzYwAAAAAAAAAAAAoAAABudWxsc19sYXN0AAAAAAAAAQAAAAAAAAD/////AAAAAA==", + "column_default_expression": "/////0gCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADwAQAAtAEAAEgBAAAEAAAAMP7//wAAAA0kAAAAOAAAAAQAAAAFAAAA/AAAAMwAAACYAAAAYAAAACAAAAASAAAAZXZhbHVhdGlvbl9jb250ZXh0AABE/v//eP7//wAAAAYQAAAALAAAAAQAAAAAAAAAGAAAAHByZXNlcnZlX2luc2VydGlvbl9vcmRlcgAAAACA/v//tP7//wAAAAUQAAAAJAAAAAQAAAAAAAAAEgAAAGRlZmF1bHRfbnVsbF9vcmRlcgAAtP7//+j+//8AAAAFEAAAACAAAAAEAAAAAAAAAA0AAABkZWZhdWx0X29yZGVyAAAA5P7//xj///8AAAAFEAAAABwAAAAEAAAAAAAAAAkAAABjb2xsYXRpb24AAAAQ////RP///wAAAAUQAAAAHAAAAAQAAAAAAAAACAAAAHRpbWV6b25lAAAAADz///9w////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAGz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAApP///9j///8AAAAFEAAAABgAAAAEAAAAAAAAAAcAAABwcm9maWxlAMz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAGAAAAAQAAAAAAAAAAwAAAHNxbAAEAAQABAAAAP////+YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACwAAAAAAAAAAAACgAYAAwABAAIAAoAAAC8AQAAEAAAAAEAAAAAAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAHwAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAwAAAAAAAAAUAAAAAAAAAANAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAIAAAAAAAAAGgAAAAAAAAAAwAAAAAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAIAAAAAAAAAHgAAAAAAAAABgAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAIAAAAAAAAAIgAAAAAAAAAAwAAAAAAAACQAAAAAAAAAAAAAAAAAAAAkAAAAAAAAAAIAAAAAAAAAJgAAAAAAAAACgAAAAAAAACoAAAAAAAAAAAAAAAAAAAAqAAAAAAAAAABAAAAAAAAAAAAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAANDIAAAAAAAAAAAAAHwAAAHZnaS5leHByZXNzaW9uLmR1Y2tkYi5zY2FsYXIudjEAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAADAAAAVVRDAAAAAAAAAAAABgAAAGJpbmFyeQAAAAAAAAMAAABhc2MAAAAAAAAAAAAKAAAAbnVsbHNfbGFzdAAAAAAAAAEAAAAAAAAA/////wAAAAA=", + "column_generated_expression": "/////0gCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADwAQAAtAEAAEgBAAAEAAAAMP7//wAAAA0kAAAAOAAAAAQAAAAFAAAA/AAAAMwAAACYAAAAYAAAACAAAAASAAAAZXZhbHVhdGlvbl9jb250ZXh0AABE/v//eP7//wAAAAYQAAAALAAAAAQAAAAAAAAAGAAAAHByZXNlcnZlX2luc2VydGlvbl9vcmRlcgAAAACA/v//tP7//wAAAAUQAAAAJAAAAAQAAAAAAAAAEgAAAGRlZmF1bHRfbnVsbF9vcmRlcgAAtP7//+j+//8AAAAFEAAAACAAAAAEAAAAAAAAAA0AAABkZWZhdWx0X29yZGVyAAAA5P7//xj///8AAAAFEAAAABwAAAAEAAAAAAAAAAkAAABjb2xsYXRpb24AAAAQ////RP///wAAAAUQAAAAHAAAAAQAAAAAAAAACAAAAHRpbWV6b25lAAAAADz///9w////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAGz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAApP///9j///8AAAAFEAAAABgAAAAEAAAAAAAAAAcAAABwcm9maWxlAMz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAGAAAAAQAAAAAAAAAAwAAAHNxbAAEAAQABAAAAP////+YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAC4AAAAAAAAAAAACgAYAAwABAAIAAoAAAC8AQAAEAAAAAEAAAAAAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAACQAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAHwAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAIAAAAAAAAAEgAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAwAAAAAAAAAWAAAAAAAAAANAAAAAAAAAGgAAAAAAAAAAAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAaAAAAAAAAAAIAAAAAAAAAHAAAAAAAAAAAwAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAIAAAAAAAAAIAAAAAAAAAABgAAAAAAAACIAAAAAAAAAAAAAAAAAAAAiAAAAAAAAAAIAAAAAAAAAJAAAAAAAAAAAwAAAAAAAACYAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAIAAAAAAAAAKAAAAAAAAAACgAAAAAAAACwAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAABAAAAAAAAAAAAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAImEuYiIgKyAxAAAAAAAAAAAAAAAfAAAAdmdpLmV4cHJlc3Npb24uZHVja2RiLnNjYWxhci52MQAAAAAAAgAAAAAAAAAHAAAADQAAAAAAAABvbmUudHdvbmVzdGVkAAAAAAAAAAMAAABVVEMAAAAAAAAAAAAGAAAAYmluYXJ5AAAAAAAAAwAAAGFzYwAAAAAAAAAAAAoAAABudWxsc19sYXN0AAAAAAAAAQAAAAAAAAD/////AAAAAA==", + "column_expression_metadata": "/////6AQAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAABoCAAABAAAALL3//8AAAECFAAAADQIAAAIAAAAGAAAAAAAAAAIAAAAY29tcHV0ZWQAAAAAAgAAAEAAAAAEAAAArPf//xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAADk9///qAcAAAQAAACYBwAALy8vLy8wZ0NBQUFRQUFBQUFBQUtBQXdBQmdBRkFBZ0FDZ0FBQUFBQkJBQU1BQUFBQ0FBSUFBQUFCQUFJQUFBQUJBQUFBQVFBQUFEd0FRQUF0QUVBQUVnQkFBQUVBQUFBTVA3Ly93QUFBQTBrQUFBQU9BQUFBQVFBQUFBRkFBQUEvQUFBQU13QUFBQ1lBQUFBWUFBQUFDQUFBQUFTQUFBQVpYWmhiSFZoZEdsdmJsOWpiMjUwWlhoMEFBQkUvdi8vZVA3Ly93QUFBQVlRQUFBQUxBQUFBQVFBQUFBQUFBQUFHQUFBQUhCeVpYTmxjblpsWDJsdWMyVnlkR2x2Ymw5dmNtUmxjZ0FBQUFDQS92Ly90UDcvL3dBQUFBVVFBQUFBSkFBQUFBUUFBQUFBQUFBQUVnQUFBR1JsWm1GMWJIUmZiblZzYkY5dmNtUmxjZ0FBdFA3Ly8raisvLzhBQUFBRkVBQUFBQ0FBQUFBRUFBQUFBQUFBQUEwQUFBQmtaV1poZFd4MFgyOXlaR1Z5QUFBQTVQNy8veGovLy84QUFBQUZFQUFBQUJ3QUFBQUVBQUFBQUFBQUFBa0FBQUJqYjJ4c1lYUnBiMjRBQUFBUS8vLy9SUC8vL3dBQUFBVVFBQUFBSEFBQUFBUUFBQUFBQUFBQUNBQUFBSFJwYldWNmIyNWxBQUFBQUR6Ly8vOXcvLy8vQUFBQURCUUFBQUFnQUFBQUJBQUFBQUVBQUFBb0FBQUFDd0FBQUhOamFHVnRZVjl3WVhSb0FHei8vLzhRQUJRQUNBQUdBQWNBREFBQUFCQUFFQUFBQUFBQUFRVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUJBQUFBR2wwWlcwQUFBQUFwUC8vLzlqLy8vOEFBQUFGRUFBQUFCZ0FBQUFFQUFBQUFBQUFBQWNBQUFCd2NtOW1hV3hsQU16Ly8vOFFBQlFBQ0FBQUFBY0FEQUFBQUJBQUVBQUFBQUFBQUFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQXdBQUFITnhiQUFFQUFRQUJBQUFBUC8vLy8rWUFnQUFGQUFBQUFBQUFBQU1BQllBQmdBRkFBZ0FEQUFNQUFBQUFBTUVBQmdBQUFDNEFBQUFBQUFBQUFBQUNnQVlBQXdBQkFBSUFBb0FBQUM4QVFBQUVBQUFBQUVBQUFBQUFBQUFBQUFBQUJvQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBSUFBQUFBQUFBQUFnQUFBQUFBQUFBQ1FBQUFBQUFBQUFZQUFBQUFBQUFBQUFBQUFBQUFBQUFHQUFBQUFBQUFBQUlBQUFBQUFBQUFDQUFBQUFBQUFBQUh3QUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFJQUFBQUFBQUFBRWdBQUFBQUFBQUFBQUFBQUFBQUFBQklBQUFBQUFBQUFBd0FBQUFBQUFBQVdBQUFBQUFBQUFBTkFBQUFBQUFBQUdnQUFBQUFBQUFBQUFBQUFBQUFBQUJvQUFBQUFBQUFBQUFBQUFBQUFBQUFhQUFBQUFBQUFBQUlBQUFBQUFBQUFIQUFBQUFBQUFBQUF3QUFBQUFBQUFCNEFBQUFBQUFBQUFBQUFBQUFBQUFBZUFBQUFBQUFBQUFJQUFBQUFBQUFBSUFBQUFBQUFBQUFCZ0FBQUFBQUFBQ0lBQUFBQUFBQUFBQUFBQUFBQUFBQWlBQUFBQUFBQUFBSUFBQUFBQUFBQUpBQUFBQUFBQUFBQXdBQUFBQUFBQUNZQUFBQUFBQUFBQUFBQUFBQUFBQUFtQUFBQUFBQUFBQUlBQUFBQUFBQUFLQUFBQUFBQUFBQUNnQUFBQUFBQUFDd0FBQUFBQUFBQUFBQUFBQUFBQUFBc0FBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBS0FBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQWdBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFKQUFBQUltRXVZaUlnS3lBeEFBQUFBQUFBQUFBQUFBQWZBQUFBZG1kcExtVjRjSEpsYzNOcGIyNHVaSFZqYTJSaUxuTmpZV3hoY2k1Mk1RQUFBQUFBQWdBQUFBQUFBQUFIQUFBQURRQUFBQUFBQUFCdmJtVXVkSGR2Ym1WemRHVmtBQUFBQUFBQUFBTUFBQUJWVkVNQUFBQUFBQUFBQUFBR0FBQUFZbWx1WVhKNUFBQUFBQUFBQXdBQUFHRnpZd0FBQUFBQUFBQUFBQW9BQUFCdWRXeHNjMTlzWVhOMEFBQUFBQUFBQVFBQUFBQUFBQUQvLy8vL0FBQUFBQT09AAAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQAIAA4ACAAHAAgAAAAAAAABQAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAA7AcAAAgAAAAQAAAAAAAAAAMAAABhLmIAAQAAAAwAAAAIAAwABAAIAAgAAACcBwAABAAAAIwHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUN3QUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFBZ0FBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUF3QUFBQUFBQUFBVUFBQUFBQUFBQUFOQUFBQUFBQUFBR0FBQUFBQUFBQUFBQUFBQUFBQUFBQmdBQUFBQUFBQUFBQUFBQUFBQUFBQVlBQUFBQUFBQUFBSUFBQUFBQUFBQUdnQUFBQUFBQUFBQXdBQUFBQUFBQUJ3QUFBQUFBQUFBQUFBQUFBQUFBQUFjQUFBQUFBQUFBQUlBQUFBQUFBQUFIZ0FBQUFBQUFBQUJnQUFBQUFBQUFDQUFBQUFBQUFBQUFBQUFBQUFBQUFBZ0FBQUFBQUFBQUFJQUFBQUFBQUFBSWdBQUFBQUFBQUFBd0FBQUFBQUFBQ1FBQUFBQUFBQUFBQUFBQUFBQUFBQWtBQUFBQUFBQUFBSUFBQUFBQUFBQUpnQUFBQUFBQUFBQ2dBQUFBQUFBQUNvQUFBQUFBQUFBQUFBQUFBQUFBQUFxQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBZ0FBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUNBQUFBTkRJQUFBQUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUlBQUFBQUFBQUFCd0FBQUEwQUFBQUFBQUFBYjI1bExuUjNiMjVsYzNSbFpBQUFBQUFBQUFBREFBQUFWVlJEQUFBQUFBQUFBQUFBQmdBQUFHSnBibUZ5ZVFBQUFBQUFBQU1BQUFCaGMyTUFBQUFBQUFBQUFBQUtBQUFBYm5Wc2JITmZiR0Z6ZEFBQUFBQUFBQUVBQUFBQUFBQUEvLy8vL3dBQUFBQT0AAAAAFQAAAHZnaS5jb2x1bW4uZGVmYXVsdC52MQAAAAgADAAIAAcACAAAAAAAAAFAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAAgAAAAAAAAAAAACgAYAAwABAAIAAoAAABcAAAAEAAAAAEAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAAQAAAAAAAAAYAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "sort_key": "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", + "layout": "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", + "empty_layout": "/////3ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAArP///wQAAAADAAAA2AAAAGwAAAAUAAAAEAAYAAgAAAAHAAwAEAAUABAAAAAAAAAFFAAAAEQAAAAkAAAABAAAAAAAAAAIAAAAY292ZXJhZ2UAAAAACAAIAAAABAAIAAAADAAAAAgADAAIAAcACAAAAAAAAAEQAAAALP///6j///8AAAAMFAAAACAAAAAEAAAAAQAAABgAAAAJAAAAc29ydGVkX2J5AAAAXP///5T///8AAAEEEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAIT///8QABQACAAAAAcADAAAABAAEAAAAAAAAAwUAAAAJAAAAAQAAAABAAAALAAAAAwAAABwYXJ0aXRpb25fYnkAAAAAyP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBBAAAAAcAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAEAAQABAAAAP////+oAAAAFAAAAAAAAAAMABQABgAFAAgADAAMAAAAAAIEABQAAAAQAAAAAAAAAAgACgAAAAQACAAAABAAAAAAAAoAGAAMAAQACAAKAAAATAAAABAAAAABAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAgAAABBTExfUk9XU/////9oAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAAoAAAAAAAAAAAACgAYAAwABAAIAAoAAADcAAAAEAAAAAEAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAABAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAgAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "layout_change": "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", + "layout_clear": "/////2ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAA7P7//wQAAAADAAAA4AAAAGAAAAAEAAAAdP///wAAAQwUAAAAIAAAAAQAAAABAAAAGAAAAAkAAABzb3J0ZWRfYnkAAAAM////pP///wAAAQQQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAANP///8z///8AAAEMFAAAACQAAAAEAAAAAQAAACwAAAAMAAAAcGFydGl0aW9uX2J5AAAAAGj///8QABQACAAGAAcADAAAABAAEAAAAAAAAQQQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAoP///xAAGAAIAAAABwAMABAAFAAQAAAAAAAABRQAAABEAAAAIAAAAAQAAAAAAAAABAAAAG1vZGUAAAAACAAIAAAABAAIAAAADAAAAAgADAAIAAcACAAAAAAAAAEQAAAABAAEAAQAAAAAAAAA/////6gAAAAUAAAAAAAAAAwAFAAGAAUACAAMAAwAAAAAAgQAFAAAABgAAAAAAAAACAAKAAAABAAIAAAAEAAAAAAACgAYAAwABAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAADQAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAADQAAAEZVVFVSRV9XUklURVMAAAD/////aAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAMAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA3AAAABAAAAABAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAAEAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAABAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "table_create": "/////3gDAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAA0P3//wQAAAALAAAAFAMAAJgCAABsAgAAQAIAAOgBAABwAQAANAEAAAgBAABcAAAAMAAAAAQAAABg/f//AAABBBAAAAAYAAAABAAAAAAAAAAGAAAAbGF5b3V0AAAM/f//iP3//wAAAQQQAAAAGAAAAAQAAAAAAAAABwAAAG9wdGlvbnMANP3//7D9//8AAAERFAAAABwAAAAEAAAAAQAAABQAAAAEAAAAdGFncwAAAABg/f//pP3//wAAAA0YAAAAIAAAAAQAAAACAAAAQAAAABQAAAAHAAAAZW50cmllcwCQ/f//DP7//wAAAQUQAAAAGAAAAAQAAAAAAAAABQAAAHZhbHVlAAAAuP3///z9//8AAAAFEAAAABQAAAAEAAAAAAAAAAMAAABrZXkA3P3//1j+//8AAAEFEAAAABgAAAAEAAAAAAAAAAcAAABjb21tZW50AAT+//+A/v//AAABBBAAAAAoAAAABAAAAAAAAAAXAAAAdHJhbnNhY3Rpb25fb3BhcXVlX2RhdGEAPP7//4D+//8AAAAMFAAAACwAAAAEAAAAAQAAACQAAAAWAAAAY29uc3RyYWludF9kZWZpbml0aW9ucwAAeP7///T+//8AAAEEEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAKD+//8QABgACAAAAAcADAAQABQAEAAAAAAAAAUUAAAARAAAACQAAAAEAAAAAAAAAAsAAABvbl9jb25mbGljdAAIAAgAAAAEAAgAAAAMAAAACAAMAAgABwAIAAAAAAAAARAAAAAE////SP///wAAAAQQAAAAGAAAAAQAAAAAAAAABwAAAGNvbHVtbnMALP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAFT///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAIT///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAvP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAoAAAABAAAAAAAAAASAAAAYXR0YWNoX29wYXF1ZV9kYXRhAAAEAAQABAAAAAAAAAD/////qAAAABQAAAAAAAAADAAUAAYABQAIAAwADAAAAAACBAAUAAAAEAAAAAAAAAAIAAoAAAAEAAgAAAAQAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAFAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAARVJST1IAAAD/////+AMAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAA4B0AAAAAAAAAAAoAGAAMAAQACAAKAAAAvAIAABAAAAABAAAAAAAAAAAAAAAqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAYAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAADgAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAA8AEAAAAAAABIAgAAAAAAAAAAAAAAAAAASAIAAAAAAAACAAAAAAAAAFACAAAAAAAAAAAAAAAAAABQAgAAAAAAAAgAAAAAAAAAWAIAAAAAAAAAAAAAAAAAAFgCAAAAAAAACAAAAAAAAABgAgAAAAAAAAAFAAAAAAAAYAcAAAAAAAABAAAAAAAAAGgHAAAAAAAACAAAAAAAAABwBwAAAAAAAAAAAAAAAAAAcAcAAAAAAAAAAAAAAAAAAHAHAAAAAAAACAAAAAAAAAB4BwAAAAAAAAQAAAAAAAAAgAcAAAAAAAAAAAAAAAAAAIAHAAAAAAAACAAAAAAAAACIBwAAAAAAAAAAAAAAAAAAiAcAAAAAAAAAAAAAAAAAAIgHAAAAAAAACAAAAAAAAACQBwAAAAAAAAQAAAAAAAAAmAcAAAAAAAAAAAAAAAAAAJgHAAAAAAAACAAAAAAAAACgBwAAAAAAAAMAAAAAAAAAqAcAAAAAAAAAAAAAAAAAAKgHAAAAAAAACAAAAAAAAACwBwAAAAAAABADAAAAAAAA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", + "table_create_legacy": "/////3gDAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAA0P3//wQAAAALAAAAFAMAAJgCAABsAgAAQAIAAOgBAABwAQAANAEAAAgBAABcAAAAMAAAAAQAAABg/f//AAABBBAAAAAYAAAABAAAAAAAAAAGAAAAbGF5b3V0AAAM/f//iP3//wAAAQQQAAAAGAAAAAQAAAAAAAAABwAAAG9wdGlvbnMANP3//7D9//8AAAERFAAAABwAAAAEAAAAAQAAABQAAAAEAAAAdGFncwAAAABg/f//pP3//wAAAA0YAAAAIAAAAAQAAAACAAAAQAAAABQAAAAHAAAAZW50cmllcwCQ/f//DP7//wAAAQUQAAAAGAAAAAQAAAAAAAAABQAAAHZhbHVlAAAAuP3///z9//8AAAAFEAAAABQAAAAEAAAAAAAAAAMAAABrZXkA3P3//1j+//8AAAEFEAAAABgAAAAEAAAAAAAAAAcAAABjb21tZW50AAT+//+A/v//AAABBBAAAAAoAAAABAAAAAAAAAAXAAAAdHJhbnNhY3Rpb25fb3BhcXVlX2RhdGEAPP7//4D+//8AAAAMFAAAACwAAAAEAAAAAQAAACQAAAAWAAAAY29uc3RyYWludF9kZWZpbml0aW9ucwAAeP7///T+//8AAAEEEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAKD+//8QABgACAAAAAcADAAQABQAEAAAAAAAAAUUAAAARAAAACQAAAAEAAAAAAAAAAsAAABvbl9jb25mbGljdAAIAAgAAAAEAAgAAAAMAAAACAAMAAgABwAIAAAAAAAAARAAAAAE////SP///wAAAAQQAAAAGAAAAAQAAAAAAAAABwAAAGNvbHVtbnMALP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAFT///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAIT///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAvP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAoAAAABAAAAAAAAAASAAAAYXR0YWNoX29wYXF1ZV9kYXRhAAAEAAQABAAAAAAAAAD/////qAAAABQAAAAAAAAADAAUAAYABQAIAAwADAAAAAACBAAUAAAAEAAAAAAAAAAIAAoAAAAEAAgAAAAQAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAFAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAARVJST1IAAAD/////+AMAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAwAIAAAAAAAAAAAoAGAAMAAQACAAKAAAAvAIAABAAAAABAAAAAAAAAAAAAAAqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAYAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAADgAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAA8AEAAAAAAABIAgAAAAAAAAAAAAAAAAAASAIAAAAAAAACAAAAAAAAAFACAAAAAAAAAAAAAAAAAABQAgAAAAAAAAgAAAAAAAAAWAIAAAAAAAAAAAAAAAAAAFgCAAAAAAAABAAAAAAAAABgAgAAAAAAAAAAAAAAAAAAYAIAAAAAAAABAAAAAAAAAGgCAAAAAAAACAAAAAAAAABwAgAAAAAAAAAAAAAAAAAAcAIAAAAAAAABAAAAAAAAAHgCAAAAAAAACAAAAAAAAACAAgAAAAAAAAAAAAAAAAAAgAIAAAAAAAABAAAAAAAAAIgCAAAAAAAACAAAAAAAAACQAgAAAAAAAAAAAAAAAAAAkAIAAAAAAAAAAAAAAAAAAJACAAAAAAAACAAAAAAAAACYAgAAAAAAAAAAAAAAAAAAmAIAAAAAAAAAAAAAAAAAAJgCAAAAAAAACAAAAAAAAACgAgAAAAAAAAAAAAAAAAAAoAIAAAAAAAABAAAAAAAAAKgCAAAAAAAACAAAAAAAAACwAgAAAAAAAAAAAAAAAAAAsAIAAAAAAAABAAAAAAAAALgCAAAAAAAACAAAAAAAAADAAgAAAAAAAAAAAAAAAAAAAAAAABAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAA8AEAAP/////oAQAAEAAAAAAACgAOAAYABQAIAAoAAAAAAQQAEAAAAAAACgAMAAAABAAIAAoAAABIAAAABAAAAAEAAAAEAAAAoP7//yQAAAAEAAAAFAAAAGNhdGFsb2ctY29udHJhY3RzL3YxAAAAAAgAAABwcm92aWRlcgAAAAACAAAA8AAAAAQAAAAq////AAABDRgAAABUAAAACAAAABgAAAABAAAAXAAAAAYAAAByZWNvcmQAAAEAAAAEAAAAGP///xwAAAAEAAAADgAAAGV4YW1wbGUucmVjb3JkAAAHAAAAbG9naWNhbACU////AAASABgACAAAAAcADAAAABAAFAASAAAAAAAABRQAAABMAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAABAAAAIj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAAGAAAAbmVzdGVkAAAEAAYABAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAaAAAAAgAAAAYAAAAAAAAAAgAAABvcHRpb25hbAAAAAABAAAADAAAAAgADAAEAAgACAAAACAAAAAEAAAAEAAAAGV4YW1wbGUub3B0aW9uYWwAAAAABwAAAGxvZ2ljYWwACAAMAAgABwAIAAAAAAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "table_info": "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AAAAAAAAAAAYAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAAEAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAgAAAAAAAAAMAAAAAAAAAADAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAADAAAAAAAAABoAAAAAAAAAA0AAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAACAAAAAAAAACAAAAAAAAAAPABAAAAAAAAcAIAAAAAAAAAAAAAAAAAAHACAAAAAAAACAAAAAAAAAB4AgAAAAAAAAAAAAAAAAAAeAIAAAAAAAAIAAAAAAAAAIACAAAAAAAAOA0AAAAAAAC4DwAAAAAAAAAAAAAAAAAAuA8AAAAAAAAIAAAAAAAAAMAPAAAAAAAACgAAAAAAAADQDwAAAAAAAAAAAAAAAAAA0A8AAAAAAAAIAAAAAAAAANgPAAAAAAAAAAAAAAAAAADYDwAAAAAAAAAAAAAAAAAA2A8AAAAAAAAEAAAAAAAAAOAPAAAAAAAAAAAAAAAAAADgDwAAAAAAAAAAAAAAAAAA4A8AAAAAAAAEAAAAAAAAAOgPAAAAAAAAAAAAAAAAAADoDwAAAAAAAAAAAAAAAAAA6A8AAAAAAAABAAAAAAAAAPAPAAAAAAAAAQAAAAAAAAD4DwAAAAAAAAgAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAQAAAAAAAAAIEAAAAAAAAAgAAAAAAAAAEBAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAQAAAAAAAAAYEAAAAAAAAAgAAAAAAAAAIBAAAAAAAAAAAAAAAAAAACAQAAAAAAAAAQAAAAAAAAAoEAAAAAAAAAgAAAAAAAAAMBAAAAAAAAAAAAAAAAAAADAQAAAAAAAAAQAAAAAAAAA4EAAAAAAAAAgAAAAAAAAAQBAAAAAAAAABAAAAAAAAAEgQAAAAAAAACAAAAAAAAABQEAAAAAAAAAEAAAAAAAAAWBAAAAAAAAAIAAAAAAAAAGAQAAAAAAAAAAAAAAAAAABgEAAAAAAAAAEAAAAAAAAAaBAAAAAAAAAIAAAAAAAAAHAQAAAAAAAAAAAAAAAAAABwEAAAAAAAAAAAAAAAAAAAcBAAAAAAAAAIAAAAAAAAAHgQAAAAAAAAAAAAAAAAAAB4EAAAAAAAAAgAAAAAAAAAgBAAAAAAAAAAAAAAAAAAAIAQAAAAAAAADAAAAAAAAACQEAAAAAAAAAAAAAAAAAAAkBAAAAAAAAAQAAAAAAAAAKAQAAAAAAAAEQAAAAAAAAC4EAAAAAAAAAAAAAAAAAAAuBAAAAAAAAAIAAAAAAAAAMAQAAAAAAAAMAUAAAAAAADwFQAAAAAAAAAAAAAAAAAA8BUAAAAAAAAIAAAAAAAAAPgVAAAAAAAAGBMAAAAAAAAAAAAAHwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABAAAAGtlcHQAAAAAAAAAAAEAAAAAAAAABAAAAHRlYW0AAAAAAAAAAAMAAABvbmUAAAAAAAAAAAAOAAAAdGFibGUud2l0aC5kb3QAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAA8AEAAP/////oAQAAEAAAAAAACgAOAAYABQAIAAoAAAAAAQQAEAAAAAAACgAMAAAABAAIAAoAAABIAAAABAAAAAEAAAAEAAAAoP7//yQAAAAEAAAAFAAAAGNhdGFsb2ctY29udHJhY3RzL3YxAAAAAAgAAABwcm92aWRlcgAAAAACAAAA8AAAAAQAAAAq////AAABDRgAAABUAAAACAAAABgAAAABAAAAXAAAAAYAAAByZWNvcmQAAAEAAAAEAAAAGP///xwAAAAEAAAADgAAAGV4YW1wbGUucmVjb3JkAAAHAAAAbG9naWNhbACU////AAASABgACAAAAAcADAAAABAAFAASAAAAAAAABRQAAABMAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAABAAAAIj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAAGAAAAbmVzdGVkAAAEAAYABAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAaAAAAAgAAAAYAAAAAAAAAAgAAABvcHRpb25hbAAAAAABAAAADAAAAAgADAAEAAgACAAAACAAAAAEAAAAEAAAAGV4YW1wbGUub3B0aW9uYWwAAAAABwAAAGxvZ2ljYWwACAAMAAgABwAIAAAAAAAAASAAAAAAAAAAAQAAAAAAAAA4DQAA/////6ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAACP///wQAAAAFAAAAUAEAABABAAC4AAAAUAAAABQAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQQQAAAAKAAAAAQAAAAAAAAAFQAAAHJlZmVyZW5jZWRfY29uc3RyYWludAAAAOj+//+s////AAAABRQAAABEAAAAJAAAAAQAAAAAAAAACgAAAHZhbGlkYXRpb24AAAgAEAAIAAQACAAAAAwAAAABAAAAAAAAAKT///8AAAABEAAAADz///8QABgACAAAAAcADAAQABQAEAAAAAAAAAUUAAAARAAAACQAAAAEAAAAAAAAAAsAAABlbmZvcmNlbWVudAAIAAgAAAAEAAgAAAAMAAAACAAMAAgABwAIAAAAAAAAARAAAACg////1P///wAAAAQQAAAAHAAAAAQAAAAAAAAACgAAAGRlZmluaXRpb24AAMz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAQQAAAAGAAAAAQAAAAAAAAAAwAAAHJlZgAEAAQABAAAAP////+oAAAAFAAAAAAAAAAMABQABgAFAAgADAAMAAAAAAIEABQAAAAQAAAAAAAAAAgACgAAAAQACAAAABAAAAAAAAoAGAAMAAQACAAKAAAATAAAABAAAAABAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAgAAABFTkZPUkNFRP////+wAAAAFAAAAAAAAAAMABQABgAFAAgADAAMAAAAAAIEABQAAAAQAAAAAAAAAAgAEgAIAAQACAAAABgAAAABAAAAAAAAAAAACgAYAAwABAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAEFMTF9ST1dT/////3gBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAIAIAAAAAAAAAAAKABgADAAEAAgACgAAAOwAAAAQAAAAAQAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAABQAwAAAAAAAFgDAAAAAAAAAAAAAAAAAABYAwAAAAAAAAgAAAAAAAAAYAMAAAAAAAAABQAAAAAAAGAIAAAAAAAAAAAAAAAAAABgCAAAAAAAAAIAAAAAAAAAaAgAAAAAAAAAAAAAAAAAAGgIAAAAAAAAAgAAAAAAAABwCAAAAAAAAAEAAAAAAAAAeAgAAAAAAAAIAAAAAAAAAIAIAAAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAFADAAD/////OAEAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAABAAAANwAAABgAAAANAAAAAQAAAB8////AAABBRAAAAAcAAAA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", + "object_ref": "/////zgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADcAAAAYAAAADQAAAAEAAAAfP///wAAAQUQAAAAHAAAAAQAAAAAAAAACwAAAG93bmVyX3RhYmxlADj///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABg////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACQ////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAMj///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAHAAAAAQAAAAAAAAABAAAAGtpbmQAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAYAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAUAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAACwAAAAAAAABQAAAAAAAAAAEAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAUAAAB0YWJsZQAAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAACwAAAHRhYmxlLiJuYW1lAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "trigger_ref": "/////zgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADcAAAAYAAAADQAAAAEAAAAfP///wAAAQUQAAAAHAAAAAQAAAAAAAAACwAAAG93bmVyX3RhYmxlADj///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABg////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACQ////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAMj///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAHAAAAAQAAAAAAAAABAAAAGtpbmQAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAaAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAcAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAACQAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAACwAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAB0cmlnZ2VyAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAACQAAAG9uLmNoYW5nZQAAAAAAAAAAAAAACwAAAHRhYmxlLiJuYW1lAAAAAAD/////AAAAAA==", + "object_dependency": "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", + "dependency_snapshot": "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", + "empty_dependency_snapshot": "/////1ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAMAAADgAAAAZAAAAAQAAAA8////AAAADBQAAAAkAAAABAAAAAEAAAAcAAAADAAAAGRlcGVuZGVuY2llcwAAAAB0////rP///wAAAQQQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAnP///5j///8AAAAMFAAAABwAAAAEAAAAAQAAACQAAAAHAAAAb2JqZWN0cwDI////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEEEAAAABwAAAAEAAAAAAAAAAQAAABpdGVtAAAAAAQABAAEAAAAEAAUAAgAAAAHAAwAAAAQABAAAAAAAAACEAAAACgAAAAEAAAAAAAAAA8AAABjYXRhbG9nX3ZlcnNpb24ACAAMAAgABwAIAAAAAAAAAUAAAAD/////aAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAKAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA3AAAABAAAAABAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAABAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "index_ref": "/////zgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADcAAAAYAAAADQAAAAEAAAAfP///wAAAQUQAAAAHAAAAAQAAAAAAAAACwAAAG93bmVyX3RhYmxlADj///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABg////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACQ////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAMj///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAHAAAAAQAAAAAAAAABAAAAGtpbmQAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAYAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAUAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAACQAAAAAAAABQAAAAAAAAAAEAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAUAAABpbmRleAAAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAACQAAAGtleS5pbmRleAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "dependency_membership": "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", + "dependency_column_literal": "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", + "dependency_column_nested": "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", + "dependency_generated": "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", + "dependency_generated_chain": "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", + "dependency_column_snapshot": "/////1ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAMAAADgAAAAZAAAAAQAAAA8////AAAADBQAAAAkAAAABAAAAAEAAAAcAAAADAAAAGRlcGVuZGVuY2llcwAAAAB0////rP///wAAAQQQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAnP///5j///8AAAAMFAAAABwAAAAEAAAAAQAAACQAAAAHAAAAb2JqZWN0cwDI////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEEEAAAABwAAAAEAAAAAAAAAAQAAABpdGVtAAAAAAQABAAEAAAAEAAUAAgAAAAHAAwAAAAQABAAAAAAAAACEAAAACgAAAAEAAAAAAAAAA8AAABjYXRhbG9nX3ZlcnNpb24ACAAMAAgABwAIAAAAAAAAAUAAAAD/////aAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAyCgAAAAAAAAAAAoAGAAMAAQACAAKAAAA3AAAABAAAAABAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAwAAAAAAAAAIAAAAAAAAABwBgAAAAAAAJAGAAAAAAAAAAAAAAAAAACQBgAAAAAAAAgAAAAAAAAAmAYAAAAAAAAAAAAAAAAAAJgGAAAAAAAAEAAAAAAAAACoBgAAAAAAACAiAAAAAAAAAAAAAAUAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAACsAAAAAAAAAAAAAAAIAAAAAAAAAOAMAAHAGAAAAAAAA/////zgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADcAAAAYAAAADQAAAAEAAAAfP///wAAAQUQAAAAHAAAAAQAAAAAAAAACwAAAG93bmVyX3RhYmxlADj///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABg////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACQ////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAMj///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAHAAAAAQAAAAAAAAABAAAAGtpbmQAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAYAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAUAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAACwAAAAAAAABQAAAAAAAAAAEAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAUAAAB0YWJsZQAAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAACwAAAHRhYmxlLiJuYW1lAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAD/////OAEAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAABAAAANwAAABgAAAANAAAAAQAAAB8////AAABBRAAAAAcAAAABAAAAAAAAAALAAAAb3duZXJfdGFibGUAOP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAGD///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAJD///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAyP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABRAAAAAcAAAABAAAAAAAAAAEAAAAa2luZAAAAAAEAAQABAAAAP////+IAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAABgAAAAAAAAAAAACgAYAAwABAAIAAoAAAD8AAAAEAAAAAEAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABQAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAJAAAAAAAAAFAAAAAAAAAAAQAAAAAAAABYAAAAAAAAAAgAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABQAAAGluZGV4AAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAAJAAAAa2V5LmluZGV4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAADAAAAAAAAAFALAADAFgAAICIAAP////8QAgAAEAAAAAAACgAMAAYABQAIAAoAAAAAAQQADAAAAAgACAAAAAQACAAAAAQAAAAGAAAArAEAAHABAAA8AQAAEAEAAGwAAAAEAAAANP///wAAAQwUAAAALAAAAAQAAAABAAAAJAAAABUAAABkZXBlbmRlbnRfY29sdW1uX3BhdGgAAAB8/v//cP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAApP7//5j///8AAAEMFAAAACwAAAAEAAAAAQAAACQAAAAUAAAAc3ViamVjdF9jb2x1bW5fcGF0aHMAAAAA4P7//9T///8AAAEMFAAAABwAAAAEAAAAAQAAACQAAAAEAAAAaXRlbQAAAAAM////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAET///+A////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAa2luZAAAAABs////qP///wAAAAYQAAAAIAAAAAQAAAAAAAAADAAAAGJsb2Nrc19hbHRlcgAAAACc////2P///wAAAAQQAAAAGAAAAAQAAAAAAAAABwAAAHN1YmplY3QAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAgAAAABAAAAAAAAAAJAAAAZGVwZW5kZW50AAAABAAEAAQAAAAAAAAA/////1gCAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAANAGAAAAAAAAAAAKABgADAAEAAgACgAAAIwBAAAQAAAAAQAAAAAAAAAAAAAAFwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAA4AwAAAAAAAEADAAAAAAAAAAAAAAAAAABAAwAAAAAAAAgAAAAAAAAASAMAAAAAAAA4AwAAAAAAAIAGAAAAAAAAAAAAAAAAAACABgAAAAAAAAEAAAAAAAAAiAYAAAAAAAAAAAAAAAAAAIgGAAAAAAAACAAAAAAAAACQBgAAAAAAAAgAAAAAAAAAmAYAAAAAAAAAAAAAAAAAAJgGAAAAAAAACAAAAAAAAACgBgAAAAAAAAAAAAAAAAAAoAYAAAAAAAAIAAAAAAAAAKgGAAAAAAAAAAAAAAAAAACoBgAAAAAAAAgAAAAAAAAAsAYAAAAAAAADAAAAAAAAALgGAAAAAAAAAQAAAAAAAADABgAAAAAAAAgAAAAAAAAAyAYAAAAAAAAAAAAAAAAAAMgGAAAAAAAACAAAAAAAAADQBgAAAAAAAAAAAAAAAAAAAAAAAAkAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAMAAP////84AQAAEAAAAAAACgAMAAYABQAIAAoAAAAAAQQADAAAAAgACAAAAAQACAAAAAQAAAAEAAAA3AAAAGAAAAA0AAAABAAAAHz///8AAAEFEAAAABwAAAAEAAAAAAAAAAsAAABvd25lcl90YWJsZQA4////cP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAG5hbWUAAAAAYP///5j///8AAAAMFAAAACAAAAAEAAAAAQAAACgAAAALAAAAc2NoZW1hX3BhdGgAkP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAADI////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAFEAAAABwAAAAEAAAAAAAAAAQAAABraW5kAAAAAAQABAAEAAAA/////4gBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAGAAAAAAAAAAAAAKABgADAAEAAgACgAAAPwAAAAQAAAAAQAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAFAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAADAAAAAAAAAAoAAAAAAAAAA0AAAAAAAAAOAAAAAAAAAAAAAAAAAAAADgAAAAAAAAACAAAAAAAAABAAAAAAAAAAAkAAAAAAAAAUAAAAAAAAAABAAAAAAAAAFgAAAAAAAAACAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAFAAAAaW5kZXgAAAAAAAAAAgAAAAAAAAAHAAAADQAAAAAAAABvbmUudHdvbmVzdGVkAAAAAAAAAAkAAABrZXkuaW5kZXgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAAAAAAADgDAAD/////OAEAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAABAAAANwAAABgAAAANAAAAAQAAAB8////AAABBRAAAAAcAAAABAAAAAAAAAALAAAAb3duZXJfdGFibGUAOP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAGD///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAJD///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAyP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABRAAAAAcAAAABAAAAAAAAAAEAAAAa2luZAAAAAAEAAQABAAAAP////+IAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAABgAAAAAAAAAAAACgAYAAwABAAIAAoAAAD8AAAAEAAAAAEAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABQAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAALAAAAAAAAAFAAAAAAAAAAAQAAAAAAAABYAAAAAAAAAAgAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABQAAAHRhYmxlAAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAALAAAAdGFibGUuIm5hbWUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAEAAAAAAAAAAAAAAAgAAAByZXF1aXJlcwAAAAABAAAAAAAAAAEAAAAAAAAAAwAAAGEuYgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAD/////EAIAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAABgAAAKwBAABwAQAAPAEAABABAABsAAAABAAAADT///8AAAEMFAAAACwAAAAEAAAAAQAAACQAAAAVAAAAZGVwZW5kZW50X2NvbHVtbl9wYXRoAAAAfP7//3D///8AAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAKT+//+Y////AAABDBQAAAAsAAAABAAAAAEAAAAkAAAAFAAAAHN1YmplY3RfY29sdW1uX3BhdGhzAAAAAOD+///U////AAABDBQAAAAcAAAABAAAAAEAAAAkAAAABAAAAGl0ZW0AAAAADP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAABE////gP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAGtpbmQAAAAAbP///6j///8AAAAGEAAAACAAAAAEAAAAAAAAAAwAAABibG9ja3NfYWx0ZXIAAAAAnP///9j///8AAAAEEAAAABgAAAAEAAAAAAAAAAcAAABzdWJqZWN0AMT///8QABQACAAAAAcADAAAABAAEAAAAAAAAAQQAAAAIAAAAAQAAAAAAAAACQAAAGRlcGVuZGVudAAAAAQABAAEAAAAAAAAAP////9YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAADwBgAAAAAAAAAACgAYAAwABAAIAAoAAACMAQAAEAAAAAEAAAAAAAAAAAAAABcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAOAMAAAAAAABAAwAAAAAAAAAAAAAAAAAAQAMAAAAAAAAIAAAAAAAAAEgDAAAAAAAAOAMAAAAAAACABgAAAAAAAAAAAAAAAAAAgAYAAAAAAAABAAAAAAAAAIgGAAAAAAAAAAAAAAAAAACIBgAAAAAAAAgAAAAAAAAAkAYAAAAAAAAIAAAAAAAAAJgGAAAAAAAAAAAAAAAAAACYBgAAAAAAAAgAAAAAAAAAoAYAAAAAAAAAAAAAAAAAAKAGAAAAAAAADAAAAAAAAACwBgAAAAAAAAAAAAAAAAAAsAYAAAAAAAAQAAAAAAAAAMAGAAAAAAAADAAAAAAAAADQBgAAAAAAAAAAAAAAAAAA0AYAAAAAAAAIAAAAAAAAANgGAAAAAAAAAAAAAAAAAADYBgAAAAAAAAgAAAAAAAAA4AYAAAAAAAAJAAAAAAAAAAAAAAAJAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAADgDAAD/////OAEAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAABAAAANwAAABgAAAANAAAAAQAAAB8////AAABBRAAAAAcAAAABAAAAAAAAAALAAAAb3duZXJfdGFibGUAOP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAGD///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAJD///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAyP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABRAAAAAcAAAABAAAAAAAAAAEAAAAa2luZAAAAAAEAAQABAAAAP////+IAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAABgAAAAAAAAAAAACgAYAAwABAAIAAoAAAD8AAAAEAAAAAEAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABQAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAALAAAAAAAAAFAAAAAAAAAAAQAAAAAAAABYAAAAAAAAAAgAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABQAAAHRhYmxlAAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAALAAAAdGFibGUuIm5hbWUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAA4AwAA/////zgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADcAAAAYAAAADQAAAAEAAAAfP///wAAAQUQAAAAHAAAAAQAAAAAAAAACwAAAG93bmVyX3RhYmxlADj///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABg////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACQ////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAMj///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAHAAAAAQAAAAAAAAABAAAAGtpbmQAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAYAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAUAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAACwAAAAAAAABQAAAAAAAAAAEAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAUAAAB0YWJsZQAAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAACwAAAHRhYmxlLiJuYW1lAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAABAAAAAAAAAAAAAAAIAAAAcmVxdWlyZXMAAAAAAgAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAwAAAAkAAAAMAAAAYS5icmVjb3JkYS5iAAAAAAAAAAABAAAAAAAAAAkAAABnZW5lcmF0ZWQAAAAAAAAA/////wAAAAD/////EAIAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAABgAAAKwBAABwAQAAPAEAABABAABsAAAABAAAADT///8AAAEMFAAAACwAAAAEAAAAAQAAACQAAAAVAAAAZGVwZW5kZW50X2NvbHVtbl9wYXRoAAAAfP7//3D///8AAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAKT+//+Y////AAABDBQAAAAsAAAABAAAAAEAAAAkAAAAFAAAAHN1YmplY3RfY29sdW1uX3BhdGhzAAAAAOD+///U////AAABDBQAAAAcAAAABAAAAAEAAAAkAAAABAAAAGl0ZW0AAAAADP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAABE////gP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAGtpbmQAAAAAbP///6j///8AAAAGEAAAACAAAAAEAAAAAAAAAAwAAABibG9ja3NfYWx0ZXIAAAAAnP///9j///8AAAAEEAAAABgAAAAEAAAAAAAAAAcAAABzdWJqZWN0AMT///8QABQACAAAAAcADAAAABAAEAAAAAAAAAQQAAAAIAAAAAQAAAAAAAAACQAAAGRlcGVuZGVudAAAAAQABAAEAAAAAAAAAP////9YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAADgBgAAAAAAAAAACgAYAAwABAAIAAoAAACMAQAAEAAAAAEAAAAAAAAAAAAAABcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAOAMAAAAAAABAAwAAAAAAAAAAAAAAAAAAQAMAAAAAAAAIAAAAAAAAAEgDAAAAAAAAOAMAAAAAAACABgAAAAAAAAAAAAAAAAAAgAYAAAAAAAABAAAAAAAAAIgGAAAAAAAAAAAAAAAAAACIBgAAAAAAAAgAAAAAAAAAkAYAAAAAAAAIAAAAAAAAAJgGAAAAAAAAAAAAAAAAAACYBgAAAAAAAAgAAAAAAAAAoAYAAAAAAAAAAAAAAAAAAKAGAAAAAAAACAAAAAAAAACoBgAAAAAAAAAAAAAAAAAAqAYAAAAAAAAIAAAAAAAAALAGAAAAAAAACQAAAAAAAADABgAAAAAAAAAAAAAAAAAAwAYAAAAAAAAIAAAAAAAAAMgGAAAAAAAAAAAAAAAAAADIBgAAAAAAAAgAAAAAAAAA0AYAAAAAAAAQAAAAAAAAAAAAAAAJAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAADgDAAD/////OAEAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAABAAAANwAAABgAAAANAAAAAQAAAB8////AAABBRAAAAAcAAAABAAAAAAAAAALAAAAb3duZXJfdGFibGUAOP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAGD///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAJD///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAyP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABRAAAAAcAAAABAAAAAAAAAAEAAAAa2luZAAAAAAEAAQABAAAAP////+IAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAABgAAAAAAAAAAAACgAYAAwABAAIAAoAAAD8AAAAEAAAAAEAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABQAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAALAAAAAAAAAFAAAAAAAAAAAQAAAAAAAABYAAAAAAAAAAgAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABQAAAHRhYmxlAAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAALAAAAdGFibGUuIm5hbWUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAA4AwAA/////zgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADcAAAAYAAAADQAAAAEAAAAfP///wAAAQUQAAAAHAAAAAQAAAAAAAAACwAAAG93bmVyX3RhYmxlADj///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABg////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACQ////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAMj///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAHAAAAAQAAAAAAAAABAAAAGtpbmQAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAYAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAUAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAACwAAAAAAAABQAAAAAAAAAAEAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAUAAAB0YWJsZQAAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAACwAAAHRhYmxlLiJuYW1lAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAABAAAAAAAAAAAAAAAIAAAAcmVxdWlyZXMAAAAAAQAAAAAAAAABAAAAAAAAAAkAAABnZW5lcmF0ZWQAAAAAAAAAAAAAAAEAAAAAAAAAEAAAAHNlY29uZC5nZW5lcmF0ZWT/////AAAAAP////8AAAAA", + "attach_default_schema_none": "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AAAAAAEAAAAAAAAASAAAAAAAAAAIAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAAAAAAAAAAAABYAAAAAAAAAAQAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAgAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAABAAAAAAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAACAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAEAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAEAAAAAAAAAiAAAAAAAAAAIAAAAAAAAAJAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAAAAAAAAAAAAkAAAAAAAAAAIAAAAAAAAAJgAAAAAAAAAAAAAAAAAAACYAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAEAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAEAAAAAAAAAKgAAAAAAAAAAAAAAAAAAACoAAAAAAAAAAAAAAAAAAAAqAAAAAAAAAABAAAAAAAAALAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAgAAAAAAAAAuAAAAAAAAAAAAAAAAAAAALgAAAAAAAAABAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAACAAAAAAAAADIAAAAAAAAAAAAAAAAAAAAyAAAAAAAAAABAAAAAAAAANAAAAAAAAAACAAAAAAAAADYAAAAAAAAAAAAAAAAAAAA2AAAAAAAAAABAAAAAAAAAOAAAAAAAAAACAAAAAAAAADoAAAAAAAAAAAAAAAAAAAA6AAAAAAAAAAAAAAAAAAAAOgAAAAAAAAAAQAAAAAAAADwAAAAAAAAAAEAAAAAAAAA+AAAAAAAAAAIAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAEAAAAAAAAACAEAAAAAAAAIAAAAAAAAABABAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAEAAAAAAAAAGAEAAAAAAAABAAAAAAAAACABAAAAAAAACAAAAAAAAAAoAQAAAAAAAAAAAAAAAAAAKAEAAAAAAAABAAAAAAAAADABAAAAAAAACAAAAAAAAAA4AQAAAAAAAAAAAAAAAAAAOAEAAAAAAAAIAAAAAAAAAEABAAAAAAAAAAAAAAAAAABAAQAAAAAAAAEAAAAAAAAASAEAAAAAAAAIAAAAAAAAAFABAAAAAAAAAAAAAAAAAABQAQAAAAAAAAgAAAAAAAAAWAEAAAAAAAAAAAAAAAAAAFgBAAAAAAAAAQAAAAAAAABgAQAAAAAAAAgAAAAAAAAAaAEAAAAAAAAAAAAAAAAAAGgBAAAAAAAAAQAAAAAAAABwAQAAAAAAAAgAAAAAAAAAeAEAAAAAAAAAAAAAAAAAAHgBAAAAAAAAAQAAAAAAAACAAQAAAAAAAAgAAAAAAAAAiAEAAAAAAAAAAAAAAAAAAIgBAAAAAAAAAQAAAAAAAACQAQAAAAAAAAEAAAAAAAAAmAEAAAAAAAAIAAAAAAAAAKABAAAAAAAAAAAAAAAAAACgAQAAAAAAAAEAAAAAAAAAqAEAAAAAAAAIAAAAAAAAALABAAAAAAAAAAAAAAAAAACwAQAAAAAAAAEAAAAAAAAAuAEAAAAAAAAIAAAAAAAAAMABAAAAAAAAAAAAAAAAAADAAQAAAAAAAAEAAAAAAAAAyAEAAAAAAAAIAAAAAAAAANABAAAAAAAAAAAAAAAAAADQAQAAAAAAAAEAAAAAAAAA2AEAAAAAAAAIAAAAAAAAAOABAAAAAAAAAQAAAAAAAADoAQAAAAAAAAgAAAAAAAAAAAAAACsAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAoAAABhdHRhY2htZW50AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "attach_default_schema_main": "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AAAAAAAAAAAAAAAAQAAAAAAAAAAIAAAAAAAAAEgAAAAAAAAABAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAAAAAAAAAAAABYAAAAAAAAAAQAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAgAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAABAAAAAAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAACAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAEAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAEAAAAAAAAAiAAAAAAAAAAIAAAAAAAAAJAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAAAAAAAAAAAAkAAAAAAAAAAIAAAAAAAAAJgAAAAAAAAAAAAAAAAAAACYAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAEAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAEAAAAAAAAAKgAAAAAAAAAAAAAAAAAAACoAAAAAAAAAAAAAAAAAAAAqAAAAAAAAAABAAAAAAAAALAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAgAAAAAAAAAuAAAAAAAAAAAAAAAAAAAALgAAAAAAAAABAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAACAAAAAAAAADIAAAAAAAAAAAAAAAAAAAAyAAAAAAAAAABAAAAAAAAANAAAAAAAAAACAAAAAAAAADYAAAAAAAAAAAAAAAAAAAA2AAAAAAAAAABAAAAAAAAAOAAAAAAAAAACAAAAAAAAADoAAAAAAAAAAAAAAAAAAAA6AAAAAAAAAAAAAAAAAAAAOgAAAAAAAAAAQAAAAAAAADwAAAAAAAAAAEAAAAAAAAA+AAAAAAAAAAIAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAEAAAAAAAAACAEAAAAAAAAIAAAAAAAAABABAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAEAAAAAAAAAGAEAAAAAAAABAAAAAAAAACABAAAAAAAACAAAAAAAAAAoAQAAAAAAAAAAAAAAAAAAKAEAAAAAAAABAAAAAAAAADABAAAAAAAACAAAAAAAAAA4AQAAAAAAAAAAAAAAAAAAOAEAAAAAAAAIAAAAAAAAAEABAAAAAAAAAAAAAAAAAABAAQAAAAAAAAEAAAAAAAAASAEAAAAAAAAIAAAAAAAAAFABAAAAAAAAAAAAAAAAAABQAQAAAAAAAAgAAAAAAAAAWAEAAAAAAAAAAAAAAAAAAFgBAAAAAAAAAQAAAAAAAABgAQAAAAAAAAgAAAAAAAAAaAEAAAAAAAAAAAAAAAAAAGgBAAAAAAAAAQAAAAAAAABwAQAAAAAAAAgAAAAAAAAAeAEAAAAAAAAAAAAAAAAAAHgBAAAAAAAAAQAAAAAAAACAAQAAAAAAAAgAAAAAAAAAiAEAAAAAAAAAAAAAAAAAAIgBAAAAAAAAAQAAAAAAAACQAQAAAAAAAAEAAAAAAAAAmAEAAAAAAAAIAAAAAAAAAKABAAAAAAAAAAAAAAAAAACgAQAAAAAAAAEAAAAAAAAAqAEAAAAAAAAIAAAAAAAAALABAAAAAAAAAAAAAAAAAACwAQAAAAAAAAEAAAAAAAAAuAEAAAAAAAAIAAAAAAAAAMABAAAAAAAAAAAAAAAAAADAAQAAAAAAAAEAAAAAAAAAyAEAAAAAAAAIAAAAAAAAANABAAAAAAAAAAAAAAAAAADQAQAAAAAAAAEAAAAAAAAA2AEAAAAAAAAIAAAAAAAAAOABAAAAAAAAAQAAAAAAAADoAQAAAAAAAAgAAAAAAAAAAAAAACsAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAoAAABhdHRhY2htZW50AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABwAAAAAAAAABAAAAAAAAAAAAAAAEAAAAbWFpbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "attach_default_schema_dotted": "/////1gJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAABUAAADsCAAAoAgAAGQIAAAoCAAA5AcAAKQHAABwBwAABAcAAKQGAABEBgAAGAYAAGwFAAAsBQAAyAQAAIwEAABQBAAADAQAAMwDAACQAwAAVAMAAAQAAADc+P//AAABDTAAAABAAAAABAAAAAgAAAAIAwAApAIAAEQCAAAQAgAA3AEAAKQBAABYAAAAHAAAAA4AAABxdWVyeV9wdXNoZG93bgAAhPf//8j3//8AAAACEAAAACAAAAAEAAAAAAAAAA8AAABtYXhfcXVlcnlfYnl0ZXMAsPj//wAAAAFAAAAAAPj//wAAAA0kAAAAQAAAAAQAAAAFAAAABAEAANQAAACgAAAAaAAAACgAAAAaAAAAZGVmYXVsdF9ldmFsdWF0aW9uX2NvbnRleHQAAAz4//9Q+P//AAAABhAAAAAsAAAABAAAAAAAAAAYAAAAcHJlc2VydmVfaW5zZXJ0aW9uX29yZGVyAAAAAEj4//+M+P//AAAABRAAAAAkAAAABAAAAAAAAAASAAAAZGVmYXVsdF9udWxsX29yZGVyAAB8+P//wPj//wAAAAUQAAAAIAAAAAQAAAAAAAAADQAAAGRlZmF1bHRfb3JkZXIAAACs+P//8Pj//wAAAAUQAAAAHAAAAAQAAAAAAAAACQAAAGNvbGxhdGlvbgAAANj4//8c+f//AAAABRAAAAAcAAAABAAAAAAAAAAIAAAAdGltZXpvbmUAAAAABPn//0j5//8AAAAFEAAAACQAAAAEAAAAAAAAABMAAABjYXBhYmlsaXR5X3JldmlzaW9uADj5//98+f//AAAABRAAAAAgAAAABAAAAAAAAAAPAAAAYmFja2VuZF92ZXJzaW9uAGj5//+s+f//AAAABRAAAAAgAAAABAAAAAAAAAAOAAAAYmFja2VuZF9lbmdpbmUAAJj5///c+f//AAAADBQAAAAkAAAABAAAAAEAAAAcAAAADwAAAGV4ZWN1dGlvbl9tb2RlcwDM+f//dPv//wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAA9Pn//zj6//8AAAAMFAAAACgAAAAEAAAAAQAAACAAAAARAAAAc2VtYW50aWNfcHJvZmlsZXMAAAAs+v//1Pv//wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAVPr//5j6//8AAAAFEAAAABwAAAAEAAAAAAAAAAgAAABlbmNvZGluZwAAAACA+v//KPz//wAAAQQQAAAAKAAAAAQAAAAAAAAAFQAAAHNlbGVjdGVkX2NhcGFiaWxpdGllcwAAALj6//9g/P//AAABBBAAAAAoAAAABAAAAAAAAAAUAAAAY2F0YWxvZ19jYXBhYmlsaXRpZXMAAAAA8Pr//zT7//8AAAAGEAAAACwAAAAEAAAAAAAAABkAAABzdXBwb3J0c19jYXRhbG9nX2NvbnRlbnRzAAAALPv//9T8//8AAAEFEAAAADAAAAAEAAAAAAAAAB8AAAByZXNvbHZlZF9pbXBsZW1lbnRhdGlvbl92ZXJzaW9uAGz7//8U/f//AAABBRAAAAAoAAAABAAAAAAAAAAVAAAAcmVzb2x2ZWRfZGF0YV92ZXJzaW9uAAAApPv//+j7//8AAAAFEAAAACgAAAAEAAAAAAAAABYAAABnbG9iYWxfZnVuY3Rpb25fcHJlZml4AADc+///IPz//wAAAAwUAAAAKAAAAAQAAAABAAAAIAAAABAAAABnbG9iYWxfZnVuY3Rpb25zAAAAABT8//+8/f//AAABBBAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAA8/P//gPz//wAAAAYQAAAALAAAAAQAAAAAAAAAGgAAAHN1cHBvcnRzX2NvbHVtbl9zdGF0aXN0aWNzAAB4/P//vPz//wAAABEUAAAAHAAAAAQAAAABAAAAFAAAAAQAAAB0YWdzAAAAAKT8///o/P//AAAADRgAAAAgAAAABAAAAAIAAABAAAAAFAAAAAcAAABlbnRyaWVzANT8//98/v//AAABBRAAAAAYAAAABAAAAAAAAAAFAAAAdmFsdWUAAAD8/P//QP3//wAAAAUQAAAAFAAAAAQAAAAAAAAAAwAAAGtleQAg/f//yP7//wAAAQUQAAAAGAAAAAQAAAAAAAAABwAAAGNvbW1lbnQASP3//4z9//8AAAAMFAAAACQAAAAEAAAAAQAAABwAAAAPAAAAYXR0YWNoX2NhdGFsb2dzAHz9//8k////AAABBBAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAACk/f//6P3//wAAAAwUAAAAJAAAAAQAAAABAAAAHAAAAAwAAABzZWNyZXRfdHlwZXMAAAAA2P3//4D///8AAAEEEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAAD+//9E/v//AAAADBQAAAAgAAAABAAAAAEAAAAYAAAACAAAAHNldHRpbmdzAAAAADD+///Y////AAABBBAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAABY/v//EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAACAAAAAEAAAAAAAAAA4AAABkZWZhdWx0X3NjaGVtYQAAmP7//9z+//8AAAAGEAAAACwAAAAEAAAAAAAAABsAAABhdHRhY2hfb3BhcXVlX2RhdGFfcmVxdWlyZWQA1P7//xj///8AAAACEAAAACgAAAAEAAAAAAAAAA8AAABjYXRhbG9nX3ZlcnNpb24ACAAMAAgABwAIAAAAAAAAAUAAAABY////AAAABhAAAAAoAAAABAAAAAAAAAAWAAAAY2F0YWxvZ192ZXJzaW9uX2Zyb3plbgAATP///5D///8AAAAGEAAAACgAAAAEAAAAAAAAABQAAABzdXBwb3J0c190aW1lX3RyYXZlbAAAAACE////yP///wAAAAYQAAAAKAAAAAQAAAAAAAAAFQAAAHN1cHBvcnRzX3RyYW5zYWN0aW9ucwAAALz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAQQAAAAKAAAAAQAAAAAAAAAEgAAAGF0dGFjaF9vcGFxdWVfZGF0YQAABAAEAAQAAAAAAAAA/////7gJAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAPgBAAAAAAAAAAAKABgADAAEAAgACgAAAMwGAAAQAAAAAQAAAAAAAAAAAAAAawAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAKAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAEAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAQAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAABAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAgAAAAAAAAAOAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAQAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAIAAAAAAAAAEgAAAAAAAAADQAAAAAAAABYAAAAAAAAAAAAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAQAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAAAAAAAAAAABoAAAAAAAAAAgAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAACAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAEAAAAAAAAAIgAAAAAAAAAAAAAAAAAAACIAAAAAAAAAAEAAAAAAAAAkAAAAAAAAAAIAAAAAAAAAJgAAAAAAAAAAAAAAAAAAACYAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAIAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAEAAAAAAAAAKgAAAAAAAAAAAAAAAAAAACoAAAAAAAAAAAAAAAAAAAAqAAAAAAAAAAEAAAAAAAAALAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAABAAAAAAAAALgAAAAAAAAAAAAAAAAAAAC4AAAAAAAAAAgAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAABAAAAAAAAADIAAAAAAAAAAAAAAAAAAAAyAAAAAAAAAAAAAAAAAAAAMgAAAAAAAAACAAAAAAAAADQAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAABAAAAAAAAANgAAAAAAAAACAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAABAAAAAAAAAOgAAAAAAAAACAAAAAAAAADwAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAQAAAAAAAAD4AAAAAAAAAAEAAAAAAAAAAAEAAAAAAAAIAAAAAAAAAAgBAAAAAAAAAAAAAAAAAAAIAQAAAAAAAAEAAAAAAAAAEAEAAAAAAAAIAAAAAAAAABgBAAAAAAAAAAAAAAAAAAAYAQAAAAAAAAEAAAAAAAAAIAEAAAAAAAABAAAAAAAAACgBAAAAAAAACAAAAAAAAAAwAQAAAAAAAAAAAAAAAAAAMAEAAAAAAAABAAAAAAAAADgBAAAAAAAACAAAAAAAAABAAQAAAAAAAAAAAAAAAAAAQAEAAAAAAAAIAAAAAAAAAEgBAAAAAAAAAAAAAAAAAABIAQAAAAAAAAEAAAAAAAAAUAEAAAAAAAAIAAAAAAAAAFgBAAAAAAAAAAAAAAAAAABYAQAAAAAAAAgAAAAAAAAAYAEAAAAAAAAAAAAAAAAAAGABAAAAAAAAAQAAAAAAAABoAQAAAAAAAAgAAAAAAAAAcAEAAAAAAAAAAAAAAAAAAHABAAAAAAAAAQAAAAAAAAB4AQAAAAAAAAgAAAAAAAAAgAEAAAAAAAAAAAAAAAAAAIABAAAAAAAAAQAAAAAAAACIAQAAAAAAAAgAAAAAAAAAkAEAAAAAAAAAAAAAAAAAAJABAAAAAAAAAQAAAAAAAACYAQAAAAAAAAEAAAAAAAAAoAEAAAAAAAAIAAAAAAAAAKgBAAAAAAAAAAAAAAAAAACoAQAAAAAAAAEAAAAAAAAAsAEAAAAAAAAIAAAAAAAAALgBAAAAAAAAAAAAAAAAAAC4AQAAAAAAAAEAAAAAAAAAwAEAAAAAAAAIAAAAAAAAAMgBAAAAAAAAAAAAAAAAAADIAQAAAAAAAAEAAAAAAAAA0AEAAAAAAAAIAAAAAAAAANgBAAAAAAAAAAAAAAAAAADYAQAAAAAAAAEAAAAAAAAA4AEAAAAAAAAIAAAAAAAAAOgBAAAAAAAAAQAAAAAAAADwAQAAAAAAAAgAAAAAAAAAAAAAACsAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAoAAABhdHRhY2htZW50AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABwAAAAAAAAABAAAAAAAAAAAAAAANAAAAc2FsZXMuYXJjaGl2ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "logical_tuple": "/////zABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABDRwAAABkAAAACAAAABwAAAACAAAAmAAAAFQAAAAEAAAAcGFpcgAAAAABAAAADAAAAAgADAAEAAgACAAAABQAAAAEAAAABQAAAHR1cGxlAAAAEwAAAHZnaS5sb2dpY2FsX3R5cGUudjEA1P///9D///8AAAEFEAAAACAAAAAEAAAAAAAAAAgAAABlbGVtZW50MgAAAAAEAAQABAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAgAAAABAAAAAAAAAAIAAAAZWxlbWVudDEAAAYACAAEAAYAAABAAAAA/////+gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABgAAAAAAAAAAAAKABgADAAEAAgACgAAAHwAAAAQAAAAAQAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAQAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAA//////////8AAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "logical_struct": "/////9gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAEAAAAoP///wAAAQ0YAAAAIAAAAAQAAAACAAAAWAAAABQAAAAEAAAAcGFpcgAAAADU////0P///wAAAQUQAAAAIAAAAAQAAAAAAAAACAAAAGVsZW1lbnQyAAAAAAQABAAEAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAACAAAAAEAAAAAAAAAAgAAABlbGVtZW50MQAABgAIAAQABgAAAEAAAAD/////6AAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAGAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAfAAAABAAAAABAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAABAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAD//////////wAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "logical_variant_sql_null": "/////4ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABDRwAAACwAAAACAAAABwAAAACAAAA7AAAALAAAAAFAAAAdmFsdWUAAAACAAAARAAAAAQAAADM////EAAAAAQAAAAAAAAAAAAAABgAAABBUlJPVzpleHRlbnNpb246bWV0YWRhdGEAAAAACAAMAAQACAAIAAAAJAAAAAQAAAAVAAAAYXJyb3cucGFycXVldC52YXJpYW50AAAAFAAAAEFSUk9XOmV4dGVuc2lvbjpuYW1lAAAAAIz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQQQAAAAGAAAAAQAAAAAAAAABQAAAHZhbHVlAAAAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAgAAAABAAAAAAAAAAIAAAAbWV0YWRhdGEAAAAABAAEAAQAAAD/////+AAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAGAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAjAAAABAAAAABAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "logical_variant_null": "/////4ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABDRwAAACwAAAACAAAABwAAAACAAAA7AAAALAAAAAFAAAAdmFsdWUAAAACAAAARAAAAAQAAADM////EAAAAAQAAAAAAAAAAAAAABgAAABBUlJPVzpleHRlbnNpb246bWV0YWRhdGEAAAAACAAMAAQACAAIAAAAJAAAAAQAAAAVAAAAYXJyb3cucGFycXVldC52YXJpYW50AAAAFAAAAEFSUk9XOmV4dGVuc2lvbjpuYW1lAAAAAIz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQQQAAAAGAAAAAQAAAAAAAAABQAAAHZhbHVlAAAAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAgAAAABAAAAAAAAAAIAAAAbWV0YWRhdGEAAAAABAAEAAQAAAD/////+AAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAIAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAjAAAABAAAAABAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAADAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAABAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAD/////AAAAAA==", + "logical_variant_absent": "/////4ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABDRwAAACwAAAACAAAABwAAAACAAAA7AAAALAAAAAFAAAAdmFsdWUAAAACAAAARAAAAAQAAADM////EAAAAAQAAAAAAAAAAAAAABgAAABBUlJPVzpleHRlbnNpb246bWV0YWRhdGEAAAAACAAMAAQACAAIAAAAJAAAAAQAAAAVAAAAYXJyb3cucGFycXVldC52YXJpYW50AAAAFAAAAEFSUk9XOmV4dGVuc2lvbjpuYW1lAAAAAIz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQQQAAAAGAAAAAQAAAAAAAAABQAAAHZhbHVlAAAAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAgAAAABAAAAAAAAAAIAAAAbWV0YWRhdGEAAAAABAAEAAQAAAD/////+AAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAIAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAjAAAABAAAAABAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAADAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAADAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAADAAAAAgAAAAAAAAD/////AAAAAA==", + "logical_variant_null_member": "/////4ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABDRwAAACwAAAACAAAABwAAAACAAAA7AAAALAAAAAFAAAAdmFsdWUAAAACAAAARAAAAAQAAADM////EAAAAAQAAAAAAAAAAAAAABgAAABBUlJPVzpleHRlbnNpb246bWV0YWRhdGEAAAAACAAMAAQACAAIAAAAJAAAAAQAAAAVAAAAYXJyb3cucGFycXVldC52YXJpYW50AAAAFAAAAEFSUk9XOmV4dGVuc2lvbjpuYW1lAAAAAIz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQQQAAAAGAAAAAQAAAAAAAAABQAAAHZhbHVlAAAAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAgAAAABAAAAAAAAAAIAAAAbWV0YWRhdGEAAAAABAAEAAQAAAD/////+AAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAKAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAjAAAABAAAAABAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAALAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAAGAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAQAHcHJlc2VudAAAAAAAAAAAAAYAAAACAQAAAQAAAP////8AAAAA", + "macro_parameter": "/////1gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAUAAAD4AAAAtAAAAHAAAAA4AAAABAAAAGz///8AAAEFEAAAACAAAAAEAAAAAAAAAA0AAABkb2N1bWVudGF0aW9uAAAAJP///5z///8AAAEEEAAAACQAAAAEAAAAAAAAABIAAABkZWZhdWx0X2V4cHJlc3Npb24AAFj////Q////AAABBBAAAAAgAAAABAAAAAAAAAANAAAAZGVmYXVsdF92YWx1ZQAAAIj///8QABQACAAGAAcADAAAABAAEAAAAAAAAQQQAAAAIAAAAAQAAAAAAAAADQAAAGRlY2xhcmVkX3R5cGUAAADI////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAFEAAAABwAAAAEAAAAAAAAAAQAAABuYW1lAAAAAAQABAAEAAAAAAAAAP////+YAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACIAgAAAAAAAAAACgAYAAwABAAIAAoAAAAMAQAAEAAAAAEAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAyAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAIAAAAAAAAAOgAAAAAAAAAcAEAAAAAAABYAgAAAAAAAAEAAAAAAAAAYAIAAAAAAAAIAAAAAAAAAGgCAAAAAAAAAAAAAAAAAABoAgAAAAAAAAAAAAAAAAAAaAIAAAAAAAAIAAAAAAAAAHACAAAAAAAAFAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAgAAABmYWxsYmFjawAAAADIAAAA/////8AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABgAAAACAAAABgAAAAAAAAACAAAAGZhbGxiYWNrAAAAAAEAAAAMAAAACAAMAAQACAAIAAAAGAAAAAQAAAAIAAAAcmV0YWluZWQAAAAABgAAAGRvbWFpbgAACAAMAAgABwAIAAAAAAAAAUAAAAAAAAAAcAEAAP/////AAAAAEAAAAAAACgAMAAYABQAIAAoAAAAAAQQADAAAAAgACAAAAAQACAAAAAQAAAABAAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAYAAAAAgAAAAYAAAAAAAAAAgAAABmYWxsYmFjawAAAAABAAAADAAAAAgADAAEAAgACAAAABgAAAAEAAAACAAAAHJldGFpbmVkAAAAAAYAAABkb21haW4AAAgADAAIAAcACAAAAAAAAAFAAAAA/////4gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABAAAAAAAAAAAAAKABgADAAEAAgACgAAADwAAAAQAAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAQSB0eXBlZCBOVUxMIGRlZmF1bHQAAAAA/////wAAAAA=", + "macro_parameter_expression": "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", + "macro_parameter_untyped": "/////1gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAUAAAD4AAAAtAAAAHAAAAA4AAAABAAAAGz///8AAAEFEAAAACAAAAAEAAAAAAAAAA0AAABkb2N1bWVudGF0aW9uAAAAJP///5z///8AAAEEEAAAACQAAAAEAAAAAAAAABIAAABkZWZhdWx0X2V4cHJlc3Npb24AAFj////Q////AAABBBAAAAAgAAAABAAAAAAAAAANAAAAZGVmYXVsdF92YWx1ZQAAAIj///8QABQACAAGAAcADAAAABAAEAAAAAAAAQQQAAAAIAAAAAQAAAAAAAAADQAAAGRlY2xhcmVkX3R5cGUAAADI////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAFEAAAABwAAAAEAAAAAAAAAAQAAABuYW1lAAAAAAQABAAEAAAAAAAAAP////+YAQAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAABQAAAAAAAAAAAACgAYAAwABAAIAAoAAAAMAQAAEAAAAAEAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAQAAAAAAAAAQAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAEAAAAAAAAAKAAAAAAAAAAIAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAEAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAEAAAAAAAAASAAAAAAAAAAIAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "macro_overload": "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", + "macro_create": "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BAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABwAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABwAAAFJFUExBQ0UA/////0gGAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAHgLAAAAAAAAAAAKABgADAAEAAgACgAAAFwEAAAQAAAAAQAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAGAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAADAAAAAAAAAAoAAAAAAAAAA0AAAAAAAAAOAAAAAAAAAAAAAAAAAAAADgAAAAAAAAACAAAAAAAAABAAAAAAAAAAA4AAAAAAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAgAAAAAAAABYAAAAAAAAAAAAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAMAAAAAAAAAHAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAUAAAAAAAAAIgAAAAAAAAADwAAAAAAAACYAAAAAAAAAAEAAAAAAAAAoAAAAAAAAAAUAAAAAAAAALgAAAAAAAAAyAEAAAAAAACAAgAAAAAAAAEAAAAAAAAAiAIAAAAAAAAUAAAAAAAAAKACAAAAAAAAcAEAAAAAAAAQBAAAAAAAAAEAAAAAAAAAGAQAAAAAAAAUAAAAAAAAADAEAAAAAAAAoAUAAAAAAADQCQAAAAAAAAEAAAAAAAAA2AkAAAAAAAAUAAAAAAAAAPAJAAAAAAAAFAAAAAAAAAAICgAAAAAAAAAAAAAAAAAACAoAAAAAAAAMAAAAAAAAABgKAAAAAAAAJQAAAAAAAABACgAAAAAAAAAAAAAAAAAAQAoAAAAAAAAMAAAAAAAAAFAKAAAAAAAAHgAAAAAAAABwCgAAAAAAAAAAAAAAAAAAcAoAAAAAAAAMAAAAAAAAAIAKAAAAAAAAAAAAAAAAAACACgAAAAAAABQAAAAAAAAAmAoAAAAAAAAaAAAAAAAAALgKAAAAAAAAAAAAAAAAAAC4CgAAAAAAAAwAAAAAAAAAyAoAAAAAAAAMAAAAAAAAANgKAAAAAAAAAAAAAAAAAADYCgAAAAAAAAAAAAAAAAAA2AoAAAAAAAAMAAAAAAAAAOgKAAAAAAAABgAAAAAAAADwCgAAAAAAAAAAAAAAAAAA8AoAAAAAAAAMAAAAAAAAAAALAAAAAAAADAAAAAAAAAAQCwAAAAAAAAAAAAAAAAAAEAsAAAAAAAAMAAAAAAAAACALAAAAAAAABgAAAAAAAAAoCwAAAAAAAAAAAAAAAAAAKAsAAAAAAAAMAAAAAAAAADgLAAAAAAAAFAAAAAAAAABQCwAAAAAAAAAAAAAAAAAAUAsAAAAAAAABAAAAAAAAAFgLAAAAAAAAAAAAAAAAAABYCwAAAAAAAAIAAAAAAAAAYAsAAAAAAAAAAAAAAAAAAGALAAAAAAAACAAAAAAAAABoCwAAAAAAAAsAAAAAAAAAAAAAABsAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAABAAAAAAAAAAQAAAAAAAAAAwAAAAAAAAAEAAAAAAAAAAMAAAAAAAAABAAAAAAAAAADAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABgAAAGF0dGFjaAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAAOAAAAbWFjcm8ud2l0aC5kb3QAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAwAAAAQAAAAAAAAAAAAAAAEAAAAJAAAADgAAAA8AAAAAAAAAeGZhbGxiYWNrc2NhbGV4AA4AAAAAAAAAAAAAAAAAAADIAAAAUAEAAMgBAAAAAAAA/////8AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABgAAAACAAAABgAAAAAAAAACAAAAGZhbGxiYWNrAAAAAAEAAAAMAAAACAAMAAQACAAIAAAAGAAAAAQAAAAIAAAAcmV0YWluZWQAAAAABgAAAGRvbWFpbgAACAAMAAgABwAIAAAAAAAAAUAAAAD/////gAAAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAAAQAAABQAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAIAAAAAQAAAAAAAAABQAAAHNjYWxlAAAACAAMAAgABwAIAAAAAAAAASAAAAAAAAAA/////3AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAEAAAB4AAAABAAEAAQAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAHABAABwAQAAcAEAAAAAAAD/////wAAAABAAAAAAAAoADAAGAAUACAAKAAAAAAEEAAwAAAAIAAgAAAAEAAgAAAAEAAAAAQAAABgAAAAAABIAGAAIAAYABwAMAAAAEAAUABIAAAAAAAECFAAAAGAAAAAIAAAAGAAAAAAAAAAIAAAAZmFsbGJhY2sAAAAAAQAAAAwAAAAIAAwABAAIAAgAAAAYAAAABAAAAAgAAAByZXRhaW5lZAAAAAAGAAAAZG9tYWluAAAIAAwACAAHAAgAAAAAAAABQAAAAP////+IAAAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAAAQAAAAAAAAAAAACgAYAAwABAAIAAoAAAA8AAAAEAAAAAEAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAABAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAoAUAAKAFAAAAAAAA/////0gCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADwAQAAtAEAAEgBAAAEAAAAMP7//wAAAA0kAAAAOAAAAAQAAAAFAAAA/AAAAMwAAACYAAAAYAAAACAAAAASAAAAZXZhbHVhdGlvbl9jb250ZXh0AABE/v//eP7//wAAAAYQAAAALAAAAAQAAAAAAAAAGAAAAHByZXNlcnZlX2luc2VydGlvbl9vcmRlcgAAAACA/v//tP7//wAAAAUQAAAAJAAAAAQAAAAAAAAAEgAAAGRlZmF1bHRfbnVsbF9vcmRlcgAAtP7//+j+//8AAAAFEAAAACAAAAAEAAAAAAAAAA0AAABkZWZhdWx0X29yZGVyAAAA5P7//xj///8AAAAFEAAAABwAAAAEAAAAAAAAAAkAAABjb2xsYXRpb24AAAAQ////RP///wAAAAUQAAAAHAAAAAQAAAAAAAAACAAAAHRpbWV6b25lAAAAADz///9w////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAGz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAApP///9j///8AAAAFEAAAABgAAAAEAAAAAAAAAAcAAABwcm9maWxlAMz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAGAAAAAQAAAAAAAAAAwAAAHNxbAAEAAQABAAAAP////+YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACoAAAAAAAAAAAACgAYAAwABAAIAAoAAAC8AQAAEAAAAAEAAAAAAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABQAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAFwAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAIAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAwAAAAAAAAASAAAAAAAAAANAAAAAAAAAFgAAAAAAAAAAAAAAAAAAABYAAAAAAAAAAAAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAwAAAAAAAABoAAAAAAAAAAAAAAAAAAAAaAAAAAAAAAAIAAAAAAAAAHAAAAAAAAAABgAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAIAAAAAAAAAIAAAAAAAAAAAwAAAAAAAACIAAAAAAAAAAAAAAAAAAAAiAAAAAAAAAAIAAAAAAAAAJAAAAAAAAAACgAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAABAAAAAAAAAAAAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAMiArIDMAAAAAAAAAFwAAAGR1Y2tkYi5tYWNyby5kZWZhdWx0LnYxAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAAAwAAAFVUQwAAAAAAAAAAAAYAAABiaW5hcnkAAAAAAAADAAAAYXNjAAAAAAAAAAAACgAAAG51bGxzX2xhc3QAAAAAAAABAAAAAAAAAP////8AAAAAAgAAAAAAAAAAAAAAAAAAABQAAAAUAAAAFAAAAAAAAABBIHR5cGVkIE5VTEwgZGVmYXVsdAAAAAAAAAAAHQAAACUAAAAAAAAAY29hbGVzY2UoeCwgZmFsbGJhY2spICogc2NhbGV1cHBlcih4KQAAAAAAAAAPAAAAHgAAAAAAAABkdWNrZGIubWFjcm8udjFkdWNrZGIubWFjcm8udjEAAAAAAAACAAAABAAAAAAAAAAAAAAABwAAAA0AAAAUAAAAGgAAAAAAAABvbmUudHdvbmVzdGVkb25lLnR3b25lc3RlZAAAAAAAAAAAAAAGAAAADAAAAAAAAABzY2FsYXJzY2FsYXIAAAAAAAAAAAMAAAAGAAAAAAAAAFVUQ1VUQwAAAAAAAAYAAAAMAAAAAAAAAGJpbmFyeWJpbmFyeQAAAAAAAAAAAwAAAAYAAAAAAAAAYXNjYXNjAAAAAAAACgAAABQAAAAAAAAAbnVsbHNfbGFzdG51bGxzX2xhc3QAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAALAAAAdHJhbnNhY3Rpb24AAAAAAP////8AAAAA", + "macro_create_single": "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BAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABQAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAEVSUk9SAAAA/////0gGAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAIABAAAAAAAAAAAKABgADAAEAAgACgAAAFwEAAAQAAAAAQAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAGAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAADAAAAAAAAAAoAAAAAAAAAA0AAAAAAAAAOAAAAAAAAAAAAAAAAAAAADgAAAAAAAAACAAAAAAAAABAAAAAAAAAAA4AAAAAAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAgAAAAAAAABYAAAAAAAAAAAAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAIAAAAAAAAAGgAAAAAAAAAAAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAaAAAAAAAAAAIAAAAAAAAAHAAAAAAAAAAAQAAAAAAAAB4AAAAAAAAAAEAAAAAAAAAgAAAAAAAAAAIAAAAAAAAAIgAAAAAAAAAAAAAAAAAAACIAAAAAAAAAAEAAAAAAAAAkAAAAAAAAAAIAAAAAAAAAJgAAAAAAAAAAAAAAAAAAACYAAAAAAAAAAEAAAAAAAAAoAAAAAAAAAAIAAAAAAAAAKgAAAAAAAAAAAAAAAAAAACoAAAAAAAAAAEAAAAAAAAAsAAAAAAAAAAIAAAAAAAAALgAAAAAAAAAAAAAAAAAAAC4AAAAAAAAAAAAAAAAAAAAuAAAAAAAAAAIAAAAAAAAAMAAAAAAAAAABQAAAAAAAADIAAAAAAAAAAAAAAAAAAAAyAAAAAAAAAAIAAAAAAAAANAAAAAAAAAADwAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAIAAAAAAAAAOgAAAAAAAAAAAAAAAAAAADoAAAAAAAAAAwAAAAAAAAA+AAAAAAAAAANAAAAAAAAAAgBAAAAAAAAAAAAAAAAAAAIAQAAAAAAAAgAAAAAAAAAEAEAAAAAAAAGAAAAAAAAABgBAAAAAAAAAAAAAAAAAAAYAQAAAAAAAAAAAAAAAAAAGAEAAAAAAAAIAAAAAAAAACABAAAAAAAAAwAAAAAAAAAoAQAAAAAAAAAAAAAAAAAAKAEAAAAAAAAIAAAAAAAAADABAAAAAAAABgAAAAAAAAA4AQAAAAAAAAAAAAAAAAAAOAEAAAAAAAAIAAAAAAAAAEABAAAAAAAAAwAAAAAAAABIAQAAAAAAAAAAAAAAAAAASAEAAAAAAAAIAAAAAAAAAFABAAAAAAAACgAAAAAAAABgAQAAAAAAAAAAAAAAAAAAYAEAAAAAAAABAAAAAAAAAGgBAAAAAAAAAAAAAAAAAABoAQAAAAAAAAIAAAAAAAAAcAEAAAAAAAABAAAAAAAAAHgBAAAAAAAACAAAAAAAAACAAQAAAAAAAAAAAAAAAAAAAAAAABsAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABgAAAGF0dGFjaAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAAOAAAAbWFjcm8ud2l0aC5kb3QAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAB4ICsgMQAAAAAAAAAPAAAAZHVja2RiLm1hY3JvLnYxAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAABgAAAHNjYWxhcgAAAAAAAAMAAABVVEMAAAAAAAAAAAAGAAAAYmluYXJ5AAAAAAAAAwAAAGFzYwAAAAAAAAAAAAoAAABudWxsc19sYXN0AAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "macro_info": "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AQAAAAAAAAAAAAAASAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAQAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAgAAAAAAAAAMAAAAAAAAAAOAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAgAAAAAAAAASAAAAAAAAAAAAAAAAAAAAEgAAAAAAAAADAAAAAAAAABYAAAAAAAAAA0AAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAAgAAAAAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAIAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAMAAAAAAAAAIgAAAAAAAAAAAAAAAAAAACIAAAAAAAAAAAAAAAAAAAAiAAAAAAAAAAUAAAAAAAAAKAAAAAAAAAADwAAAAAAAACwAAAAAAAAAAEAAAAAAAAAuAAAAAAAAAAUAAAAAAAAANAAAAAAAAAAyAEAAAAAAACYAgAAAAAAAAEAAAAAAAAAoAIAAAAAAAAUAAAAAAAAALgCAAAAAAAAmAIAAAAAAABQBQAAAAAAAAEAAAAAAAAAWAUAAAAAAAAUAAAAAAAAAHAFAAAAAAAAAAAAAAAAAABwBQAAAAAAAAEAAAAAAAAAeAUAAAAAAAAUAAAAAAAAAJAFAAAAAAAAFAAAAAAAAACoBQAAAAAAAAAAAAAAAAAAqAUAAAAAAAAMAAAAAAAAALgFAAAAAAAAJQAAAAAAAADgBQAAAAAAAAAAAAAAAAAA4AUAAAAAAAAMAAAAAAAAAPAFAAAAAAAAHgAAAAAAAAAQBgAAAAAAAAAAAAAAAAAAEAYAAAAAAAAMAAAAAAAAACAGAAAAAAAAAAAAAAAAAAAgBgAAAAAAABQAAAAAAAAAOAYAAAAAAAAaAAAAAAAAAFgGAAAAAAAAAAAAAAAAAABYBgAA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", + "macro_info_single": "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AQAAAAAAAAAAAAAASAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAQAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAgAAAAAAAAAMAAAAAAAAAAOAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAgAAAAAAAAASAAAAAAAAAAAAAAAAAAAAEgAAAAAAAAADAAAAAAAAABYAAAAAAAAAA0AAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAAgAAAAAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAIAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAIAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAIAAAAAAAAAIgAAAAAAAAAAQAAAAAAAACQAAAAAAAAAAEAAAAAAAAAmAAAAAAAAAAIAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAEAAAAAAAAAqAAAAAAAAAAIAAAAAAAAALAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAEAAAAAAAAAuAAAAAAAAAAIAAAAAAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAEAAAAAAAAAyAAAAAAAAAAIAAAAAAAAANAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAIAAAAAAAAANgAAAAAAAAABQAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAIAAAAAAAAAOgAAAAAAAAADwAAAAAAAAD4AAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAIAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAwAAAAAAAAAEAEAAAAAAAANAAAAAAAAACABAAAAAAAAAAAAAAAAAAAgAQAAAAAAAAgAAAAAAAAAKAEAAAAAAAAGAAAAAAAAADABAAAAAAAAAAAAAAAAAAAwAQAAAAAAAAAAAAAAAAAAMAEAAAAAAAAIAAAAAAAAADgBAAAAAAAAAwAAAAAAAABAAQAAAAAAAAAAAAAAAAAAQAEAAAAAAAAIAAAAAAAAAEgBAAAAAAAABgAAAAAAAABQAQAAAAAAAAAAAAAAAAAAUAEAAAAAAAAIAAAAAAAAAFgBAAAAAAAAAwAAAAAAAABgAQAAAAAAAAAAAAAAAAAAYAEAAAAAAAAIAAAAAAAAAGgBAAAAAAAACgAAAAAAAAB4AQAAAAAAAAAAAAAAAAAAeAEAAAAAAAABAAAAAAAAAAAAAAAdAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAABtYWNyby53aXRoLmRvdAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAB4ICsgMQAAAAAAAAAPAAAAZHVja2RiLm1hY3JvLnYxAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAABgAAAHNjYWxhcgAAAAAAAAMAAABVVEMAAAAAAAAAAAAGAAAAYmluYXJ5AAAAAAAAAwAAAGFzYwAAAAAAAAAAAAoAAABudWxsc19sYXN0AAAAAAAAAQAAAAAAAAD/////AAAAAA==", + "sql_statement": "/////0gCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADwAQAAtAEAAEgBAAAEAAAAMP7//wAAAA0kAAAAOAAAAAQAAAAFAAAA/AAAAMwAAACYAAAAYAAAACAAAAASAAAAZXZhbHVhdGlvbl9jb250ZXh0AABE/v//eP7//wAAAAYQAAAALAAAAAQAAAAAAAAAGAAAAHByZXNlcnZlX2luc2VydGlvbl9vcmRlcgAAAACA/v//tP7//wAAAAUQAAAAJAAAAAQAAAAAAAAAEgAAAGRlZmF1bHRfbnVsbF9vcmRlcgAAtP7//+j+//8AAAAFEAAAACAAAAAEAAAAAAAAAA0AAABkZWZhdWx0X29yZGVyAAAA5P7//xj///8AAAAFEAAAABwAAAAEAAAAAAAAAAkAAABjb2xsYXRpb24AAAAQ////RP///wAAAAUQAAAAHAAAAAQAAAAAAAAACAAAAHRpbWV6b25lAAAAADz///9w////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAGz///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAApP///9j///8AAAAFEAAAABgAAAAEAAAAAAAAAAcAAABwcm9maWxlAMz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAGAAAAAQAAAAAAAAAAwAAAHNxbAAEAAQABAAAAP////+YAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAADQAAAAAAAAAAAACgAYAAwABAAIAAoAAAC8AQAAEAAAAAEAAAAAAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAIQAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAIAAAAAAAAADgAAAAAAAAAHQAAAAAAAABYAAAAAAAAAAAAAAAAAAAAWAAAAAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAwAAAAAAAAAcAAAAAAAAAANAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAIAAAAAAAAAIgAAAAAAAAAAwAAAAAAAACQAAAAAAAAAAAAAAAAAAAAkAAAAAAAAAAIAAAAAAAAAJgAAAAAAAAABgAAAAAAAACgAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAIAAAAAAAAAKgAAAAAAAAABAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAIAAAAAAAAALgAAAAAAAAACwAAAAAAAADIAAAAAAAAAAAAAAAAAAAAyAAAAAAAAAABAAAAAAAAAAAAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAhAAAAU0VMRUNUICJhLmIiIEZST00gImNoaWxkLiIidGFibGUiAAAAAAAAAAAAAAAdAAAAdmdpLnN0YXRlbWVudC5kdWNrZGIucXVlcnkudjEAAAAAAAAAAgAAAAAAAAAHAAAADQAAAAAAAABvbmUudHdvbmVzdGVkAAAAAAAAAAMAAABVVEMAAAAAAAAAAAAGAAAAYmluYXJ5AAAAAAAABAAAAGRlc2MAAAAAAAAAAAsAAABudWxsc19maXJzdAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "constraint_ref": "/////zgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADcAAAAYAAAADQAAAAEAAAAfP///wAAAQUQAAAAHAAAAAQAAAAAAAAACwAAAG93bmVyX3RhYmxlADj///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABg////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACQ////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAMj///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAHAAAAAQAAAAAAAAABAAAAGtpbmQAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAaAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAoAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAACAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAMAAAAAAAAADAAAAAAAAAADQAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAIAAAAAAAAAEgAAAAAAAAABgAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAADAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAoAAABjb25zdHJhaW50AAAAAAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAAGAAAAa2V5LjAxAAAAAAAADAAAAGNoaWxkLiJ0YWJsZQAAAAD/////AAAAAA==", + "foreign_key_definition": "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", + "constraint_definition_not_null": "/////9gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAAcP7//wQAAAAFAAAAXAEAALAAAABsAAAANAAAAAQAAAAA////AAABBBAAAAAcAAAABAAAAAAAAAALAAAAZm9yZWlnbl9rZXkAlP7//yz///8AAAEEEAAAACQAAAAEAAAAAAAAABAAAABjaGVja19leHByZXNzaW9uAAAAAMj+//9g////AAABBRAAAAAgAAAABAAAAAAAAAANAAAAZGVjbGFyZWRfbmFtZQAAAPj+//8QABQACAAAAAcADAAAABAAEAAAAAAAAAwUAAAAJAAAAAQAAAABAAAAHAAAAAwAAABjb2x1bW5fcGF0aHMAAAAAPP///9T///8AAAEMFAAAABwAAAAEAAAAAQAAACQAAAAEAAAAaXRlbQAAAABo////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAKD///8QABgACAAAAAcADAAQABQAEAAAAAAAAAUUAAAARAAAACAAAAAEAAAAAAAAAAQAAABraW5kAAAAAAgACAAAAAQACAAAAAwAAAAIAAwACAAHAAgAAAAAAAABEAAAAAQABAAEAAAA/////6gAAAAUAAAAAAAAAAwAFAAGAAUACAAMAAwAAAAAAgQAFAAAABAAAAAAAAAACAAKAAAABAAIAAAAEAAAAAAACgAYAAwABAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAE5PVF9OVUxM/////+gBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAFgAAAAAAAAAAAAKABgADAAEAAgACgAAADwBAAAQAAAAAQAAAAAAAAAAAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAADAAAAAAAAACgAAAAAAAAAAQAAAAAAAAAwAAAAAAAAAAgAAAAAAAAAOAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAQAAAAAAAABAAAAAAAAAAAgAAAAAAAAASAAAAAAAAAAAAAAAAAAAAEgAAAAAAAAAAQAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAADAAAAYS5iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "constraint_definition_unique": "/////9gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAAcP7//wQAAAAFAAAAXAEAALAAAABsAAAANAAAAAQAAAAA////AAABBBAAAAAcAAAABAAAAAAAAAALAAAAZm9yZWlnbl9rZXkAlP7//yz///8AAAEEEAAAACQAAAAEAAAAAAAAABAAAABjaGVja19leHByZXNzaW9uAAAAAMj+//9g////AAABBRAAAAAgAAAABAAAAAAAAAANAAAAZGVjbGFyZWRfbmFtZQAAAPj+//8QABQACAAAAAcADAAAABAAEAAAAAAAAAwUAAAAJAAAAAQAAAABAAAAHAAAAAwAAABjb2x1bW5fcGF0aHMAAAAAPP///9T///8AAAEMFAAAABwAAAAEAAAAAQAAACQAAAAEAAAAaXRlbQAAAABo////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAKD///8QABgACAAAAAcADAAQABQAEAAAAAAAAAUUAAAARAAAACAAAAAEAAAAAAAAAAQAAABraW5kAAAAAAgACAAAAAQACAAAAAwAAAAIAAwACAAHAAgAAAAAAAABEAAAAAQABAAEAAAA/////6gAAAAUAAAAAAAAAAwAFAAGAAUACAAMAAwAAAAAAgQAFAAAABAAAAAAAAAACAAKAAAABAAIAAAAEAAAAAAACgAYAAwABAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABgAAAFVOSVFVRQAA/////+gBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAHAAAAAAAAAAAAAKABgADAAEAAgACgAAADwBAAAQAAAAAQAAAAAAAAAAAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAMAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAMAAAAAAAAAALAAAAAAAAAEAAAAAAAAAAAQAAAAAAAABIAAAAAAAAAAgAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAQAAAAAAAABYAAAAAAAAAAgAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAQAAAAAAAABoAAAAAAAAAAgAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAMAAAAAAAAAAAAAAAMAAAAJAAAACwAAAGEuYnJlY29yZGlkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "constraint_definition_primary": "/////9gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAAcP7//wQAAAAFAAAAXAEAALAAAABsAAAANAAAAAQAAAAA////AAABBBAAAAAcAAAABAAAAAAAAAALAAAAZm9yZWlnbl9rZXkAlP7//yz///8AAAEEEAAAACQAAAAEAAAAAAAAABAAAABjaGVja19leHByZXNzaW9uAAAAAMj+//9g////AAABBRAAAAAgAAAABAAAAAAAAAANAAAAZGVjbGFyZWRfbmFtZQAAAPj+//8QABQACAAAAAcADAAAABAAEAAAAAAAAAwUAAAAJAAAAAQAAAABAAAAHAAAAAwAAABjb2x1bW5fcGF0aHMAAAAAPP///9T///8AAAEMFAAAABwAAAAEAAAAAQAAACQAAAAEAAAAaXRlbQAAAABo////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAKD///8QABgACAAAAAcADAAQABQAEAAAAAAAAAUUAAAARAAAACAAAAAEAAAAAAAAAAQAAABraW5kAAAAAAgACAAAAAQACAAAAAwAAAAIAAwACAAHAAgAAAAAAAABEAAAAAQABAAEAAAA/////6gAAAAUAAAAAAAAAAwAFAAGAAUACAAMAAwAAAAAAgQAFAAAABgAAAAAAAAACAAKAAAABAAIAAAAEAAAAAAACgAYAAwABAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAACwAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACwAAAFBSSU1BUllfS0VZAAAAAAD/////6AEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAYAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAEAABAAAAABAAAAAAAAAAAAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAACAAAAAAAAAAgAAAAAAAAAAIAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAACAAAAAAAAAAwAAAAAAAAAA0AAAAAAAAAQAAAAAAAAAABAAAAAAAAAEgAAAAAAAAACAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAABAAAAAAAAAFgAAAAAAAAACAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAIAAABpZAAAAAAAAAAAAAANAAAAcHJpbWFyeS5sYWJlbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "constraint_definition_check": "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", + "constraint_definition_foreign": "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", + "constraint_info": "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", + "constraint_info_untrusted": "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AQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABgAAAFVOSVFVRQAA/////+gBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAHAAAAAAAAAAAAAKABgADAAEAAgACgAAADwBAAAQAAAAAQAAAAAAAAAAAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAMAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAMAAAAAAAAAALAAAAAAAAAEAAAAAAAAAAAQAAAAAAAABIAAAAAAAAAAgAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAQAAAAAAAABYAAAAAAAAAAgAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAQAAAAAAAABoAAAAAAAAAAgAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAMAAAAAAAAAAAAAAAMAAAAJAAAACwAAAGEuYnJlY29yZGlkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "constraint_change_add": "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", + "constraint_change_drop": "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", + "constraint_change_rename": "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", + "constraint_change_unname": "/////yACAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAAKP7//wQAAAAHAAAAoAEAAGgBAAAoAQAA0AAAAGQAAAA4AAAABAAAAMD+//8AAAEFEAAAACAAAAAEAAAAAAAAAA0AAABkZWNsYXJlZF9uYW1lAAAAWP7///D+//8AAAEGEAAAABgAAAAEAAAAAAAAAAcAAABjYXNjYWRlAID+//+o////AAABBRQAAABIAAAAJAAAAAQAAAAAAAAACgAAAHZhbGlkYXRpb24AAAgAFAAIAAQACAAAABAAAAACAAAAAAAAAAAAAADg/v//AAAAARAAAADY/v//EAAYAAgABgAHAAwAEAAUABAAAAAAAAEFFAAAAEQAAAAkAAAABAAAAAAAAAALAAAAZW5mb3JjZW1lbnQACAAQAAgABAAIAAAADAAAAAEAAAAAAAAARP///wAAAAEQAAAAPP///9T///8AAAEEEAAAABwAAAAEAAAAAAAAAAoAAABkZWZpbml0aW9uAABo////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEEEAAAABQAAAAEAAAAAAAAAAMAAAByZWYAnP///xAAGAAIAAAABwAMABAAFAAQAAAAAAAABRQAAABIAAAAJAAAAAQAAAAAAAAACQAAAG9wZXJhdGlvbgAAAAgACAAAAAQACAAAAAwAAAAIAAwACAAHAAgAAAAAAAABEAAAAAQABAAEAAAA/////6gAAAAUAAAAAAAAAAwAFAAGAAUACAAMAAwAAAAAAgQAFAAAABAAAAAAAAAACAAKAAAABAAIAAAAEAAAAAAACgAYAAwABAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABgAAAFJFTkFNRQAA/////6gAAAAUAAAAAAAAAAwAFAAGAAUACAAMAAwAAAAAAgQAFAAAAAgAAAAAAAAACAAWAAgABAAIAAAAHAAAAAEAAAAAAAAAAAAAAAAACgAMAAAABAAIAAoAAABAAAAABAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////+oAAAAFAAAAAAAAAAMABQABgAFAAgADAAMAAAAAAIEABQAAAAIAAAAAAAAAAgAFgAIAAQACAAAABwAAAACAAAAAAAAAAAAAAAAAAoADAAAAAQACAAKAAAAQAAAAAQAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////2AEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAoAMAAAAAAAAAAAoAGAAMAAQACAAKAAAALAEAABAAAAABAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAQAMAAAAAAABQAwAAAAAAAAEAAAAAAAAAWAMAAAAAAAAIAAAAAAAAAGADAAAAAAAAAAAAAAAAAABgAwAAAAAAAAEAAAAAAAAAaAMAAAAAAAAIAAAAAAAAAHADAAAAAAAAAQAAAAAAAAB4AwAAAAAAAAgAAAAAAAAAgAMAAAAAAAABAAAAAAAAAIgDAAAAAAAAAQAAAAAAAACQAwAAAAAAAAEAAAAAAAAAmAMAAAAAAAAIAAAAAAAAAKADAAAAAAAAAAAAAAAAAAAAAAAABwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAABAAwAA/////zgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADcAAAAYAAAADQAAAAEAAAAfP///wAAAQUQAAAAHAAAAAQAAAAAAAAACwAAAG93bmVyX3RhYmxlADj///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABg////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACQ////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAAMj///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAHAAAAAQAAAAAAAAABAAAAGtpbmQAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAaAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAoAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAACAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAMAAAAAAAAADAAAAAAAAAADQAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAIAAAAAAAAAEgAAAAAAAAABgAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAADAAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAoAAABjb25zdHJhaW50AAAAAAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAAGAAAAa2V5LjAxAAAAAAAADAAAAGNoaWxkLiJ0YWJsZQAAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "constraint_change_replace": "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", + "constraint_snapshot": "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FAAAAAAAAAAMABQABgAFAAgADAAMAAAAAAIEABQAAAAQAAAAAAAAAAgACgAAAAQACAAAABAAAAAAAAoAGAAMAAQACAAKAAAATAAAABAAAAABAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAYAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAYAAABTSU1QTEUAAP////+wAAAAFAAAAAAAAAAMABQABgAFAAgADAAMAAAAAAIEABQAAAAYAAAAAAAAAAgAEgAIAAQACAAAABgAAAABAAAAAAAAAAAACgAYAAwABAAIAAoAAABMAAAAEAAAAAEAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAACQAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACQAAAE5PX0FDVElPTgAAAAAAAAD/////sAAAABQAAAAAAAAADAAUAAYABQAIAAwADAAAAAACBAAUAAAAGAAAAAAAAAAIABIACAAEAAgAAAAYAAAAAgAAAAAAAAAAAAoAGAAMAAQACAAKAAAATAAAABAAAAABAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAkAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAkAAABOT19BQ1RJT04AAAAAAAAA/////8gBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAJgDAAAAAAAAAAAKABgADAAEAAgACgAAABwBAAAQAAAAAQAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAA4AwAAAAAAAEADAAAAAAAAAAAAAAAAAABAAwAAAAAAAAgAAAAAAAAASAMAAAAAAAAAAAAAAAAAAEgDAAAAAAAADAAAAAAAAABYAwAAAAAAAAAAAAAAAAAAWAMAAAAAAAAQAAAAAAAAAGgDAAAAAAAAEQAAAAAAAACAAwAAAAAAAAAAAAAAAAAAgAMAAAAAAAACAAAAAAAAAIgDAAAAAAAAAAAAAAAAAACIAwAA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", + "constraint_snapshot_empty": "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", + "constraints_apply": "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", + "constraints_apply_autocommit": "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", + "index_key_column": "/////9AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAAA0AAAABAAAAKT///8AAAEEEAAAABwAAAAEAAAAAAAAAAoAAABleHByZXNzaW9uAACY////0P///wAAAQwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABjb2x1bW5fcGF0aADI////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABwAAAAEAAAAAAAAAAQAAABpdGVtAAAAAAQABAAEAAAA/////wgBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAACgAAAAAAAAAAAAKABgADAAEAAgACgAAAJwAAAAQAAAAAQAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAMAAAAAAAAAGAAAAAAAAAABAAAAAAAAACAAAAAAAAAACAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAADAAAAYS5iAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "index_key_nested": "/////9AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAAA0AAAABAAAAKT///8AAAEEEAAAABwAAAAEAAAAAAAAAAoAAABleHByZXNzaW9uAACY////0P///wAAAQwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABjb2x1bW5fcGF0aADI////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABwAAAAEAAAAAAAAAAQAAABpdGVtAAAAAAQABAAEAAAA/////wgBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAADgAAAAAAAAAAAAKABgADAAEAAgACgAAAJwAAAAQAAAAAQAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAADAAAAAAAAAAYAAAAAAAAAAkAAAAAAAAAKAAAAAAAAAABAAAAAAAAADAAAAAAAAAACAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAGAAAACQAAAAAAAAByZWNvcmRhLmIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA", + "index_key_expression": "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", + "index_definition": "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", + "index_definition_primary": "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", + "index_info": "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", + "index_create": "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", + "index_create_cross_schema": "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", + "query_offer": "/////5ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAQAAADwAAAAfAAAAEgAAAAEAAAAMP///wAAAAIQAAAAKAAAAAQAAAAAAAAADwAAAG1heF9xdWVyeV9ieXRlcwAIAAwACAAHAAgAAAAAAAABQAAAAHD///8AAAAFEAAAACAAAAAEAAAAAAAAAA4AAABlbmdpbmVfdmVyc2lvbgAAKP///6D///8AAAAMFAAAACgAAAAEAAAAAQAAACAAAAARAAAAc2VtYW50aWNfcHJvZmlsZXMAAABg////mP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAiP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAADBQAAAAgAAAABAAAAAEAAAAoAAAACQAAAGVuY29kaW5ncwAAAMj///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAHAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAABAAEAAQAAAAAAAAA/////6gBAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAJAAAAAAAAAAAAAKABgADAAEAAgACgAAAAwBAAAQAAAAAQAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAA0AAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAACAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAMAAAAAAAAADgAAAAAAAAALgAAAAAAAABoAAAAAAAAAAAAAAAAAAAAaAAAAAAAAAAIAAAAAAAAAHAAAAAAAAAAFAAAAAAAAACIAAAAAAAAAAAAAAAAAAAAiAAAAAAAAAAIAAAAAAAAAAAAAAAGAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAADQAAAGR1Y2tkYi5zcWwudjEAAAAAAAAAAgAAAAAAAAAYAAAALgAAAAAAAAB2Z2kuZHVja2RiLnJlbGF0aW9uYWwudjF2Z2kucmVtb3RlLnJlYWRvbmx5LnYxAAAAAAAAFAAAAGZpeHR1cmUtY2xpZW50LWJ1aWxkAAAAAAAAEAAAAAAA/////wAAAAA=", + "query_capabilities": "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", + "query_context": "/////1ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAUAAADwAAAAsAAAAHwAAABEAAAABAAAADT///8AAAAGEAAAACwAAAAEAAAAAAAAABgAAABwcmVzZXJ2ZV9pbnNlcnRpb25fb3JkZXIAAAAANP///3D///8AAAAFEAAAACQAAAAEAAAAAAAAABIAAABkZWZhdWx0X251bGxfb3JkZXIAAGj///+k////AAAABRAAAAAgAAAABAAAAAAAAAANAAAAZGVmYXVsdF9vcmRlcgAAAJj////U////AAAABRAAAAAcAAAABAAAAAAAAAAJAAAAY29sbGF0aW9uAAAAxP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABRAAAAAgAAAABAAAAAAAAAAIAAAAdGltZXpvbmUAAAAABAAEAAQAAAD/////iAEAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAUAAAAAAAAAAAAAoAGAAMAAQACAAKAAAA/AAAABAAAAABAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAMAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAYAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAACAAAAAAAAAAoAAAAAAAAAAMAAAAAAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAACAAAAAAAAAA4AAAAAAAAAAoAAAAAAAAASAAAAAAAAAAAAAAAAAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAABQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAABVVEMAAAAAAAAAAAAGAAAAYmluYXJ5AAAAAAAAAwAAAGFzYwAAAAAAAAAAAAoAAABudWxsc19sYXN0AAAAAAAAAQAAAAAAAAD/////AAAAAA==", + "query_object_reference": "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", + "query_prepare": "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CAAAAAAAAABwAAAAAAAAAAgAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAACAAAAAAAAACAAAAAAAAAABUAAAAAAAAAmAAAAAAAAAAAAAAAAAAAAJgAAAAAAAAACAAAAAAAAACgAAAAAAAAACgCAAAAAAAAyAIAAAAAAAAAAAAAAAAAAMgCAAAAAAAAAAAAAAAAAADIAgAAAAAAAAgAAAAAAAAA0AIAAAAAAAADAAAAAAAAANgCAAAAAAAAAAAAAAAAAADYAgAAAAAAAAgAAAAAAAAA4AIAAAAAAAAGAAAAAAAAAOgCAAAAAAAAAAAAAAAAAADoAgAAAAAAAAgAAAAAAAAA8AIAAAAAAAADAAAAAAAAAPgCAAAAAAAAAAAAAAAAAAD4AgAAAAAAAAgAAAAAAAAAAAMAAAAAAAAKAAAAAAAAABADAAAAAAAAAAAAAAAAAAAQAwAAAAAAAAEAAAAAAAAAGAMAAAAAAAAAAAAAAAAAABgDAAAAAAAACAAAAAAAAAAgAwAAAAAAAAsAAAAAAAAAMAMAAAAAAAAAAAAAAAAAADADAAAAAAAACAAAAAAAAAA4AwAAAAAAAAAAAAAAAAAAOAMAAAAAAAAIAAAAAAAAAEADAAAAAAAAAAAAAAAAAABAAwAAAAAAAAAAAAAAAAAAQAMAAAAAAAAIAAAAAAAAAEgDAAAAAAAAAAAAAAAAAABIAwAAAAAAAAAAAAAAAAAASAMAAAAAAAAAAAAAAAAAAEgDAAAAAAAACAAAAAAAAABQAwAAAAAAAAUAAAAAAAAAWAMAAAAAAAAAAAAAAAAAAFgDAAAAAAAACAAAAAAAAABgAwAAAAAAAAAAAAAAAAAAYAMAAAAAAAAMAAAAAAAAAHADAAAAAAAADQAAAAAAAACAAwAAAAAAAAAAAAAAAAAAgAMAAAAAAAAIAAAAAAAAAIgDAAAAAAAACwAAAAAAAACYAwAAAAAAAAEAAAAAAAAAoAMAAAAAAAAIAAAAAAAAAKgDAAAAAAAAAAAAAAAAAACoAwAAAAAAAAAAAAAAAAAAqAMAAAAAAAAIAAAAAAAAALADAAAAAAAA8AEAAAAAAACgBQAAAAAAAAAAAAAAAAAAoAUAAAAAAAAIAAAAAAAAAKgFAAAAAAAA8AEAAAAAAACYBwAAAAAAAAAAAAAAAAAAmAcAAAAAAAAIAAAA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", + "query_accepted": "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", + "query_declined": "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", + "query_parameters": "/////yACAAAQAAAAAAAKAA4ABgAFAAgACgAAAAABBAAQAAAAAAAKAAwAAAAEAAgACgAAAIAAAAAEAAAAAgAAADwAAAAEAAAAbP7//xQAAAAEAAAABQAAAG5hbWVkAAAAFwAAAHZnaS5xdWVyeS5wYXJhbWV0ZXJzLnYxAKD+//8kAAAABAAAABQAAABjYXRhbG9nLWNvbnRyYWN0cy92MQAAAAAIAAAAcHJvdmlkZXIAAAAAAgAAAPAAAAAEAAAAKv///wAAAQ0YAAAAVAAAAAgAAAAYAAAAAQAAAFwAAAAGAAAAcmVjb3JkAAABAAAABAAAABj///8cAAAABAAAAA4AAABleGFtcGxlLnJlY29yZAAABwAAAGxvZ2ljYWwAlP///wAAEgAYAAgAAAAHAAwAAAAQABQAEgAAAAAAAAUUAAAATAAAAAgAAAAQAAAAAAAAAAMAAABhLmIAAQAAAAQAAACI////GAAAAAQAAAAIAAAAcmV0YWluZWQAAAAABgAAAG5lc3RlZAAABAAGAAQAAAAAABIAGAAIAAYABwAMAAAAEAAUABIAAAAAAAECFAAAAGgAAAAIAAAAGAAAAAAAAAAIAAAAb3B0aW9uYWwAAAAAAQAAAAwAAAAIAAwABAAIAAgAAAAgAAAABAAAABAAAABleGFtcGxlLm9wdGlvbmFsAAAAAAcAAABsb2dpY2FsAAgADAAIAAcACAAAAAAAAAEgAAAA/////+gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAACgAAAAAAAAAAAAKABgADAAEAAgACgAAAHwAAAAQAAAAAQAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAANAAAAAAAAAAAAAAADAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAAAAbGl0ZXJhbC4ibmFtZQAAAP////8AAAAA", + "query_empty_parameters": "/////4AAAAAQAAAAAAAKAA4ABgAFAAgACgAAAAABBAAQAAAAAAAKAAwAAAAEAAgACgAAAFAAAAAEAAAAAQAAAAwAAAAIAAwABAAIAAgAAAAYAAAABAAAAAoAAABwb3NpdGlvbmFsAAAXAAAAdmdpLnF1ZXJ5LnBhcmFtZXRlcnMudjEAAAAAAP////9IAAAAFAAAAAAAAAAAAAoADgAGAAUACAAKAAAAAAMEABAAAAAAAAoAFAAMAAQACAAKAAAAFAAAAAwAAAABAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "column_add_literal": "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Y2dBQUFBQ0Evdi8vdFA3Ly93QUFBQVVRQUFBQUpBQUFBQVFBQUFBQUFBQUFFZ0FBQUdSbFptRjFiSFJmYm5Wc2JGOXZjbVJsY2dBQXRQNy8vK2orLy84QUFBQUZFQUFBQUNBQUFBQUVBQUFBQUFBQUFBMEFBQUJrWldaaGRXeDBYMjl5WkdWeUFBQUE1UDcvL3hqLy8vOEFBQUFGRUFBQUFCd0FBQUFFQUFBQUFBQUFBQWtBQUFCamIyeHNZWFJwYjI0QUFBQVEvLy8vUlAvLy93QUFBQVVRQUFBQUhBQUFBQVFBQUFBQUFBQUFDQUFBQUhScGJXVjZiMjVsQUFBQUFEei8vLzl3Ly8vL0FBQUFEQlFBQUFBZ0FBQUFCQUFBQUFFQUFBQW9BQUFBQ3dBQUFITmphR1Z0WVY5d1lYUm9BR3ovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFCQUFBQUdsMFpXMEFBQUFBcFAvLy85ai8vLzhBQUFBRkVBQUFBQmdBQUFBRUFBQUFBQUFBQUFjQUFBQndjbTltYVd4bEFNei8vLzhRQUJRQUNBQUFBQWNBREFBQUFCQUFFQUFBQUFBQUFBVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUF3QUFBSE54YkFBRUFBUUFCQUFBQVAvLy8vK1lBZ0FBRkFBQUFBQUFBQUFNQUJZQUJnQUZBQWdBREFBTUFBQUFBQU1FQUJnQUFBQ3dBQUFBQUFBQUFBQUFDZ0FZQUF3QUJBQUlBQW9BQUFDOEFRQUFFQUFBQUFFQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBZ0FBQUFBQUFBQUFRQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFJQUFBQUFBQUFBQmdBQUFBQUFBQUFId0FBQUFBQUFBQTRBQUFBQUFBQUFBQUFBQUFBQUFBQU9BQUFBQUFBQUFBSUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQXdBQUFBQUFBQUFVQUFBQUFBQUFBQU5BQUFBQUFBQUFHQUFBQUFBQUFBQUFBQUFBQUFBQUFCZ0FBQUFBQUFBQUFBQUFBQUFBQUFBWUFBQUFBQUFBQUFJQUFBQUFBQUFBR2dBQUFBQUFBQUFBd0FBQUFBQUFBQndBQUFBQUFBQUFBQUFBQUFBQUFBQWNBQUFBQUFBQUFBSUFBQUFBQUFBQUhnQUFBQUFBQUFBQmdBQUFBQUFBQUNBQUFBQUFBQUFBQUFBQUFBQUFBQUFnQUFBQUFBQUFBQUlBQUFBQUFBQUFJZ0FBQUFBQUFBQUF3QUFBQUFBQUFDUUFBQUFBQUFBQUFBQUFBQUFBQUFBa0FBQUFBQUFBQUFJQUFBQUFBQUFBSmdBQUFBQUFBQUFDZ0FBQUFBQUFBQ29BQUFBQUFBQUFBQUFBQUFBQUFBQXFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUtBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFOd0FBQUFBQUFBQUFBQUFBSHdBQUFIWm5hUzVsZUhCeVpYTnphVzl1TG1SMVkydGtZaTV6WTJGc1lYSXVkakVBQUFBQUFBSUFBQUFBQUFBQUJ3QUFBQTBBQUFBQUFBQUFiMjVsTG5SM2IyNWxjM1JsWkFBQUFBQUFBQUFEQUFBQVZWUkRBQUFBQUFBQUFBQUFCZ0FBQUdKcGJtRnllUUFBQUFBQUFBTUFBQUJoYzJNQUFBQUFBQUFBQUFBS0FBQUFiblZzYkhOZmJHRnpkQUFBQUFBQUFBRUFBQUFBQUFBQS8vLy8vd0FBQUFBPQAAAAAVAAAAdmdpLmNvbHVtbi5kZWZhdWx0LnYxAAAACAAMAAgABwAIAAAAAAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "column_add_nested": "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Y25abFgybHVjMlZ5ZEdsdmJsOXZjbVJsY2dBQUFBQ0Evdi8vdFA3Ly93QUFBQVVRQUFBQUpBQUFBQVFBQUFBQUFBQUFFZ0FBQUdSbFptRjFiSFJmYm5Wc2JGOXZjbVJsY2dBQXRQNy8vK2orLy84QUFBQUZFQUFBQUNBQUFBQUVBQUFBQUFBQUFBMEFBQUJrWldaaGRXeDBYMjl5WkdWeUFBQUE1UDcvL3hqLy8vOEFBQUFGRUFBQUFCd0FBQUFFQUFBQUFBQUFBQWtBQUFCamIyeHNZWFJwYjI0QUFBQVEvLy8vUlAvLy93QUFBQVVRQUFBQUhBQUFBQVFBQUFBQUFBQUFDQUFBQUhScGJXVjZiMjVsQUFBQUFEei8vLzl3Ly8vL0FBQUFEQlFBQUFBZ0FBQUFCQUFBQUFFQUFBQW9BQUFBQ3dBQUFITmphR1Z0WVY5d1lYUm9BR3ovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFCQUFBQUdsMFpXMEFBQUFBcFAvLy85ai8vLzhBQUFBRkVBQUFBQmdBQUFBRUFBQUFBQUFBQUFjQUFBQndjbTltYVd4bEFNei8vLzhRQUJRQUNBQUFBQWNBREFBQUFCQUFFQUFBQUFBQUFBVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUF3QUFBSE54YkFBRUFBUUFCQUFBQVAvLy8vK1lBZ0FBRkFBQUFBQUFBQUFNQUJZQUJnQUZBQWdBREFBTUFBQUFBQU1FQUJnQUFBQ3dBQUFBQUFBQUFBQUFDZ0FZQUF3QUJBQUlBQW9BQUFDOEFRQUFFQUFBQUFFQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBZ0FBQUFBQUFBQUFRQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFJQUFBQUFBQUFBQmdBQUFBQUFBQUFId0FBQUFBQUFBQTRBQUFBQUFBQUFBQUFBQUFBQUFBQU9BQUFBQUFBQUFBSUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQXdBQUFBQUFBQUFVQUFBQUFBQUFBQU5BQUFBQUFBQUFHQUFBQUFBQUFBQUFBQUFBQUFBQUFCZ0FBQUFBQUFBQUFBQUFBQUFBQUFBWUFBQUFBQUFBQUFJQUFBQUFBQUFBR2dBQUFBQUFBQUFBd0FBQUFBQUFBQndBQUFBQUFBQUFBQUFBQUFBQUFBQWNBQUFBQUFBQUFBSUFBQUFBQUFBQUhnQUFBQUFBQUFBQmdBQUFBQUFBQUNBQUFBQUFBQUFBQUFBQUFBQUFBQUFnQUFBQUFBQUFBQUlBQUFBQUFBQUFJZ0FBQUFBQUFBQUF3QUFBQUFBQUFDUUFBQUFBQUFBQUFBQUFBQUFBQUFBa0FBQUFBQUFBQUFJQUFBQUFBQUFBSmdBQUFBQUFBQUFDZ0FBQUFBQUFBQ29BQUFBQUFBQUFBQUFBQUFBQUFBQXFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUtBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFOd0FBQUFBQUFBQUFBQUFBSHdBQUFIWm5hUzVsZUhCeVpYTnphVzl1TG1SMVkydGtZaTV6WTJGc1lYSXVkakVBQUFBQUFBSUFBQUFBQUFBQUJ3QUFBQTBBQUFBQUFBQUFiMjVsTG5SM2IyNWxjM1JsWkFBQUFBQUFBQUFEQUFBQVZWUkRBQUFBQUFBQUFBQUFCZ0FBQUdKcGJtRnllUUFBQUFBQUFBTUFBQUJoYzJNQUFBQUFBQUFBQUFBS0FBQUFiblZzYkhOZmJHRnpkQUFBQUFBQUFBRUFBQUFBQUFBQS8vLy8vd0FBQUFBPQAAAAAVAAAAdmdpLmNvbHVtbi5kZWZhdWx0LnYxAAAACAAMAAgABwAIAAAAAAAAASAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAALAAAAdHJhbnNhY3Rpb24AAAAAAP////8AAAAA", + "column_drop_literal": "/////0gCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAgAAADgAQAAZAEAADgBAADcAAAApAAAAGwAAABAAAAABAAAAIj+//8AAAEEEAAAACgAAAAEAAAAAAAAABcAAAB0cmFuc2FjdGlvbl9vcGFxdWVfZGF0YQBE/v//iP7//wAAAAYQAAAAGAAAAAQAAAAAAAAABwAAAGNhc2NhZGUAbP7//7D+//8AAAAGEAAAACQAAAAEAAAAAAAAABAAAABpZl9jb2x1bW5fZXhpc3RzAAAAAKD+///k/v//AAAABhAAAAAkAAAABAAAAAAAAAAQAAAAaWdub3JlX25vdF9mb3VuZAAAAADU/v//GP///wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAABjb2x1bW5fcGF0aAAE////gP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAALP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAFT///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAIT///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAvP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAoAAAABAAAAAAAAAASAAAAYXR0YWNoX29wYXF1ZV9kYXRhAAAEAAQABAAAAP////+IAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACQAAAAAAAAAAAACgAYAAwABAAIAAoAAACsAQAAEAAAAAEAAAAAAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAACAAAAAAAAABgAAAAAAAAAAMAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAAQAAAAAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAABAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAEAAAAAAAAAgAAAAAAAAAABAAAAAAAAAIgAAAAAAAAACAAAAAAAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAAAQAAAAAAAAADAAAAYS5iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "column_drop_nested": "/////0gCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAgAAADgAQAAZAEAADgBAADcAAAApAAAAGwAAABAAAAABAAAAIj+//8AAAEEEAAAACgAAAAEAAAAAAAAABcAAAB0cmFuc2FjdGlvbl9vcGFxdWVfZGF0YQBE/v//iP7//wAAAAYQAAAAGAAAAAQAAAAAAAAABwAAAGNhc2NhZGUAbP7//7D+//8AAAAGEAAAACQAAAAEAAAAAAAAABAAAABpZl9jb2x1bW5fZXhpc3RzAAAAAKD+///k/v//AAAABhAAAAAkAAAABAAAAAAAAAAQAAAAaWdub3JlX25vdF9mb3VuZAAAAADU/v//GP///wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAABjb2x1bW5fcGF0aAAE////gP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAALP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAFT///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAIT///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAvP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAoAAAABAAAAAAAAAASAAAAYXR0YWNoX29wYXF1ZV9kYXRhAAAEAAQABAAAAP////+IAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACoAAAAAAAAAAAACgAYAAwABAAIAAoAAACsAQAAEAAAAAEAAAAAAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAADAAAAAAAAABoAAAAAAAAAAoAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAABAAAAAAAAAIgAAAAAAAAAAAAAAAAAAACIAAAAAAAAAAEAAAAAAAAAkAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAACAAAAAAAAACYAAAAAAAAAAsAAAAAAAAAAAAAAAoAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAAAgAAAAAAAAABAAAACgAAAAAAAABhYi4icXVvdGVkAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAsAAAB0cmFuc2FjdGlvbgAAAAAA/////wAAAAA=", + "column_rename_literal": "/////xgCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAcAAACwAQAANAEAAAgBAACsAAAAeAAAAEAAAAAEAAAAtP7//wAAAQQQAAAAKAAAAAQAAAAAAAAAFwAAAHRyYW5zYWN0aW9uX29wYXF1ZV9kYXRhAHD+//+0/v//AAAABhAAAAAkAAAABAAAAAAAAAAQAAAAaWdub3JlX25vdF9mb3VuZAAAAACk/v//6P7//wAAAAUQAAAAIAAAAAQAAAAAAAAADwAAAG5ld19jb2x1bW5fbmFtZQDU/v//GP///wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAABjb2x1bW5fcGF0aAAE////gP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAALP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAFT///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAIT///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAvP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAoAAAABAAAAAAAAAASAAAAYXR0YWNoX29wYXF1ZV9kYXRhAAAEAAQABAAAAP////9oAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACYAAAAAAAAAAAACgAYAAwABAAIAAoAAACcAQAAEAAAAAEAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAACAAAAAAAAABgAAAAAAAAAAMAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAACAAAAAAAAABwAAAAAAAAAAkAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAQAAAAAAAACIAAAAAAAAAAEAAAAAAAAAkAAAAAAAAAAIAAAAAAAAAJgAAAAAAAAAAAAAAAAAAAAAAAAACQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAGAAAAYXR0YWNoAAAAAAAAAgAAAAAAAAAHAAAADQAAAAAAAABvbmUudHdvbmVzdGVkAAAAAAAAAA4AAAB0YWJsZS53aXRoLmRvdAAAAAAAAAEAAAAAAAAAAwAAAGEuYgAAAAAAAAAAAAkAAABuZXcuIm5hbWUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=", + "column_rename_nested": "/////xgCAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAcAAACwAQAANAEAAAgBAACsAAAAeAAAAEAAAAAEAAAAtP7//wAAAQQQAAAAKAAAAAQAAAAAAAAAFwAAAHRyYW5zYWN0aW9uX29wYXF1ZV9kYXRhAHD+//+0/v//AAAABhAAAAAkAAAABAAAAAAAAAAQAAAAaWdub3JlX25vdF9mb3VuZAAAAACk/v//6P7//wAAAAUQAAAAIAAAAAQAAAAAAAAADwAAAG5ld19jb2x1bW5fbmFtZQDU/v//GP///wAAAAwUAAAAIAAAAAQAAAABAAAAGAAAAAsAAABjb2x1bW5fcGF0aAAE////gP///wAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAALP///3D///8AAAAFEAAAABgAAAAEAAAAAAAAAAQAAABuYW1lAAAAAFT///+Y////AAAADBQAAAAgAAAABAAAAAEAAAAoAAAACwAAAHNjaGVtYV9wYXRoAIT///8QABQACAAGAAcADAAAABAAEAAAAAAAAQUQAAAAGAAAAAQAAAAAAAAABAAAAGl0ZW0AAAAAvP///xAAFAAIAAAABwAMAAAAEAAQAAAAAAAABBAAAAAoAAAABAAAAAAAAAASAAAAYXR0YWNoX29wYXF1ZV9kYXRhAAAEAAQABAAAAP////9oAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACwAAAAAAAAAAAACgAYAAwABAAIAAoAAACcAQAAEAAAAAEAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAADAAAAAAAAABoAAAAAAAAAAoAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAACAAAAAAAAACAAAAAAAAAAAkAAAAAAAAAkAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAAAQAAAAAAAACYAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAIAAAAAAAAAKAAAAAAAAAACwAAAAAAAAAAAAAACQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYXR0YWNoAAAAAAAAAgAAAAAAAAAHAAAADQAAAAAAAABvbmUudHdvbmVzdGVkAAAAAAAAAA4AAAB0YWJsZS53aXRoLmRvdAAAAAAAAAIAAAAAAAAAAQAAAAoAAAAAAAAAYWIuInF1b3RlZAAAAAAAAAAAAAAJAAAAbmV3LiJuYW1lAAAAAAAAAAEAAAAAAAAAAAAAAAsAAAB0cmFuc2FjdGlvbgAAAAAA/////wAAAAA=", + "column_type_change_literal": "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", + "column_type_change_nested": "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", + "column_default_set_literal": "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", + "column_default_set_nested": "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", + "column_default_drop_literal": "/////+gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAYAAAB8AQAAAAEAANQAAAB4AAAAQAAAAAQAAADk/v//AAABBBAAAAAoAAAABAAAAAAAAAAXAAAAdHJhbnNhY3Rpb25fb3BhcXVlX2RhdGEAoP7//+T+//8AAAAGEAAAACQAAAAEAAAAAAAAABAAAABpZ25vcmVfbm90X2ZvdW5kAAAAANT+//8Y////AAAADBQAAAAgAAAABAAAAAEAAAAYAAAACwAAAGNvbHVtbl9wYXRoAAT///+A////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAs////cP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAG5hbWUAAAAAVP///5j///8AAAAMFAAAACAAAAAEAAAAAQAAACgAAAALAAAAc2NoZW1hX3BhdGgAhP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC8////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAEEAAAACgAAAAEAAAAAAAAABIAAABhdHRhY2hfb3BhcXVlX2RhdGEAAAQABAAEAAAAAAAAAP////8oAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACAAAAAAAAAAAAACgAYAAwABAAIAAoAAABsAQAAEAAAAAEAAAAAAAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAACAAAAAAAAABgAAAAAAAAAAMAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAAQAAAAAAAABwAAAAAAAAAAEAAAAAAAAAeAAAAAAAAAAIAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAAAQAAAAAAAAADAAAAYS5iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "column_default_drop_nested": "/////+gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAYAAAB8AQAAAAEAANQAAAB4AAAAQAAAAAQAAADk/v//AAABBBAAAAAoAAAABAAAAAAAAAAXAAAAdHJhbnNhY3Rpb25fb3BhcXVlX2RhdGEAoP7//+T+//8AAAAGEAAAACQAAAAEAAAAAAAAABAAAABpZ25vcmVfbm90X2ZvdW5kAAAAANT+//8Y////AAAADBQAAAAgAAAABAAAAAEAAAAYAAAACwAAAGNvbHVtbl9wYXRoAAT///+A////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAs////cP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAG5hbWUAAAAAVP///5j///8AAAAMFAAAACAAAAAEAAAAAQAAACgAAAALAAAAc2NoZW1hX3BhdGgAhP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC8////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAEEAAAACgAAAAEAAAAAAAAABIAAABhdHRhY2hfb3BhcXVlX2RhdGEAAAQABAAEAAAAAAAAAP////8oAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACYAAAAAAAAAAAACgAYAAwABAAIAAoAAABsAQAAEAAAAAEAAAAAAAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAADAAAAAAAAABoAAAAAAAAAAoAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAIAAAAAAAAAIgAAAAAAAAACwAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAAAgAAAAAAAAABAAAACgAAAAAAAABhYi4icXVvdGVkAAAAAAAAAQAAAAAAAAAAAAAACwAAAHRyYW5zYWN0aW9uAAAAAAD/////AAAAAA==", + "column_comment_set_literal": "/////xACAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAcAAACoAQAALAEAAAABAACkAAAAeAAAAEAAAAAEAAAAvP7//wAAAQQQAAAAKAAAAAQAAAAAAAAAFwAAAHRyYW5zYWN0aW9uX29wYXF1ZV9kYXRhAHj+//+8/v//AAAABhAAAAAkAAAABAAAAAAAAAAQAAAAaWdub3JlX25vdF9mb3VuZAAAAACs/v//KP///wAAAQUQAAAAGAAAAAQAAAAAAAAABwAAAGNvbW1lbnQA1P7//xj///8AAAAMFAAAACAAAAAEAAAAAQAAABgAAAALAAAAY29sdW1uX3BhdGgABP///4D///8AAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAACz///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABU////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACE////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAALz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAQQAAAAKAAAAAQAAAAAAAAAEgAAAGF0dGFjaF9vcGFxdWVfZGF0YQAABAAEAAQAAAD/////aAIAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAkAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAnAEAABAAAAABAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAYAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAADgAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAAAAAAAAAAAABYAAAAAAAAAAgAAAAAAAAAYAAAAAAAAAADAAAAAAAAAGgAAAAAAAAAAQAAAAAAAABwAAAAAAAAAAgAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAEAAAAAAAAAgAAAAAAAAAABAAAAAAAAAIgAAAAAAAAACAAAAAAAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAkAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABgAAAGF0dGFjaAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAAOAAAAdGFibGUud2l0aC5kb3QAAAAAAAABAAAAAAAAAAMAAABhLmIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "column_comment_set_nested": "/////xACAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAcAAACoAQAALAEAAAABAACkAAAAeAAAAEAAAAAEAAAAvP7//wAAAQQQAAAAKAAAAAQAAAAAAAAAFwAAAHRyYW5zYWN0aW9uX29wYXF1ZV9kYXRhAHj+//+8/v//AAAABhAAAAAkAAAABAAAAAAAAAAQAAAAaWdub3JlX25vdF9mb3VuZAAAAACs/v//KP///wAAAQUQAAAAGAAAAAQAAAAAAAAABwAAAGNvbW1lbnQA1P7//xj///8AAAAMFAAAACAAAAAEAAAAAQAAABgAAAALAAAAY29sdW1uX3BhdGgABP///4D///8AAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAACz///9w////AAAABRAAAAAYAAAABAAAAAAAAAAEAAAAbmFtZQAAAABU////mP///wAAAAwUAAAAIAAAAAQAAAABAAAAKAAAAAsAAABzY2hlbWFfcGF0aACE////EAAUAAgABgAHAAwAAAAQABAAAAAAAAEFEAAAABgAAAAEAAAAAAAAAAQAAABpdGVtAAAAALz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAQQAAAAKAAAAAQAAAAAAAAAEgAAAGF0dGFjaF9vcGFxdWVfZGF0YQAABAAEAAQAAAD/////aAIAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAqAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAnAEAABAAAAABAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAYAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAMAAAAAAAAACgAAAAAAAAADQAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAIAAAAAAAAAEAAAAAAAAAADgAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAIAAAAAAAAAFgAAAAAAAAAAAAAAAAAAABYAAAAAAAAAAwAAAAAAAAAaAAAAAAAAAAKAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAgAAAAAAAAAgAAAAAAAAAAEAAAAAAAAAIgAAAAAAAAAAAAAAAAAAACIAAAAAAAAAAEAAAAAAAAAkAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAACAAAAAAAAACYAAAAAAAAAAsAAAAAAAAAAAAAAAkAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABgAAAGF0dGFjaAAAAAAAAAIAAAAAAAAABwAAAA0AAAAAAAAAb25lLnR3b25lc3RlZAAAAAAAAAAOAAAAdGFibGUud2l0aC5kb3QAAAAAAAACAAAAAAAAAAEAAAAKAAAAAAAAAGFiLiJxdW90ZWQAAAAAAAAAAAAABAAAAGtlcHQAAAAAAQAAAAAAAAAAAAAACwAAAHRyYW5zYWN0aW9uAAAAAAD/////AAAAAA==", + "not_null_set_literal": "/////+gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAYAAAB8AQAAAAEAANQAAAB4AAAAQAAAAAQAAADk/v//AAABBBAAAAAoAAAABAAAAAAAAAAXAAAAdHJhbnNhY3Rpb25fb3BhcXVlX2RhdGEAoP7//+T+//8AAAAGEAAAACQAAAAEAAAAAAAAABAAAABpZ25vcmVfbm90X2ZvdW5kAAAAANT+//8Y////AAAADBQAAAAgAAAABAAAAAEAAAAYAAAACwAAAGNvbHVtbl9wYXRoAAT///+A////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAs////cP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAG5hbWUAAAAAVP///5j///8AAAAMFAAAACAAAAAEAAAAAQAAACgAAAALAAAAc2NoZW1hX3BhdGgAhP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC8////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAEEAAAACgAAAAEAAAAAAAAABIAAABhdHRhY2hfb3BhcXVlX2RhdGEAAAQABAAEAAAAAAAAAP////8oAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACAAAAAAAAAAAAACgAYAAwABAAIAAoAAABsAQAAEAAAAAEAAAAAAAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAACAAAAAAAAABgAAAAAAAAAAMAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAAQAAAAAAAABwAAAAAAAAAAEAAAAAAAAAeAAAAAAAAAAIAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAAAQAAAAAAAAADAAAAYS5iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "not_null_set_nested": "/////+gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAYAAAB8AQAAAAEAANQAAAB4AAAAQAAAAAQAAADk/v//AAABBBAAAAAoAAAABAAAAAAAAAAXAAAAdHJhbnNhY3Rpb25fb3BhcXVlX2RhdGEAoP7//+T+//8AAAAGEAAAACQAAAAEAAAAAAAAABAAAABpZ25vcmVfbm90X2ZvdW5kAAAAANT+//8Y////AAAADBQAAAAgAAAABAAAAAEAAAAYAAAACwAAAGNvbHVtbl9wYXRoAAT///+A////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAs////cP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAG5hbWUAAAAAVP///5j///8AAAAMFAAAACAAAAAEAAAAAQAAACgAAAALAAAAc2NoZW1hX3BhdGgAhP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC8////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAEEAAAACgAAAAEAAAAAAAAABIAAABhdHRhY2hfb3BhcXVlX2RhdGEAAAQABAAEAAAAAAAAAP////8oAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACYAAAAAAAAAAAACgAYAAwABAAIAAoAAABsAQAAEAAAAAEAAAAAAAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAADAAAAAAAAABoAAAAAAAAAAoAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAIAAAAAAAAAIgAAAAAAAAACwAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAAAgAAAAAAAAABAAAACgAAAAAAAABhYi4icXVvdGVkAAAAAAAAAQAAAAAAAAAAAAAACwAAAHRyYW5zYWN0aW9uAAAAAAD/////AAAAAA==", + "not_null_drop_literal": "/////+gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAYAAAB8AQAAAAEAANQAAAB4AAAAQAAAAAQAAADk/v//AAABBBAAAAAoAAAABAAAAAAAAAAXAAAAdHJhbnNhY3Rpb25fb3BhcXVlX2RhdGEAoP7//+T+//8AAAAGEAAAACQAAAAEAAAAAAAAABAAAABpZ25vcmVfbm90X2ZvdW5kAAAAANT+//8Y////AAAADBQAAAAgAAAABAAAAAEAAAAYAAAACwAAAGNvbHVtbl9wYXRoAAT///+A////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAs////cP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAG5hbWUAAAAAVP///5j///8AAAAMFAAAACAAAAAEAAAAAQAAACgAAAALAAAAc2NoZW1hX3BhdGgAhP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC8////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAEEAAAACgAAAAEAAAAAAAAABIAAABhdHRhY2hfb3BhcXVlX2RhdGEAAAQABAAEAAAAAAAAAP////8oAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACAAAAAAAAAAAAACgAYAAwABAAIAAoAAABsAQAAEAAAAAEAAAAAAAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAACAAAAAAAAABgAAAAAAAAAAMAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAAQAAAAAAAABwAAAAAAAAAAEAAAAAAAAAeAAAAAAAAAAIAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAAAQAAAAAAAAADAAAAYS5iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA==", + "not_null_drop_nested": "/////+gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAYAAAB8AQAAAAEAANQAAAB4AAAAQAAAAAQAAADk/v//AAABBBAAAAAoAAAABAAAAAAAAAAXAAAAdHJhbnNhY3Rpb25fb3BhcXVlX2RhdGEAoP7//+T+//8AAAAGEAAAACQAAAAEAAAAAAAAABAAAABpZ25vcmVfbm90X2ZvdW5kAAAAANT+//8Y////AAAADBQAAAAgAAAABAAAAAEAAAAYAAAACwAAAGNvbHVtbl9wYXRoAAT///+A////AAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAs////cP///wAAAAUQAAAAGAAAAAQAAAAAAAAABAAAAG5hbWUAAAAAVP///5j///8AAAAMFAAAACAAAAAEAAAAAQAAACgAAAALAAAAc2NoZW1hX3BhdGgAhP///xAAFAAIAAYABwAMAAAAEAAQAAAAAAABBRAAAAAYAAAABAAAAAAAAAAEAAAAaXRlbQAAAAC8////EAAUAAgAAAAHAAwAAAAQABAAAAAAAAAEEAAAACgAAAAEAAAAAAAAABIAAABhdHRhY2hfb3BhcXVlX2RhdGEAAAQABAAEAAAAAAAAAP////8oAgAAFAAAAAAAAAAMABYABgAFAAgADAAMAAAAAAMEABgAAACYAAAAAAAAAAAACgAYAAwABAAIAAoAAABsAQAAEAAAAAEAAAAAAAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAANAAAAAAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAOAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAgAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAADAAAAAAAAABoAAAAAAAAAAoAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAIAAAAAAAAAIgAAAAAAAAACwAAAAAAAAAAAAAACAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAYAAABhdHRhY2gAAAAAAAACAAAAAAAAAAcAAAANAAAAAAAAAG9uZS50d29uZXN0ZWQAAAAAAAAADgAAAHRhYmxlLndpdGguZG90AAAAAAAAAgAAAAAAAAABAAAACgAAAAAAAABhYi4icXVvdGVkAAAAAAAAAQAAAAAAAAAAAAAACwAAAHRyYW5zYWN0aW9uAAAAAAD/////AAAAAA==" + }, + "column_metadata_schemas": { + "valid": { + "absent": "/////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAABwAAAAEAAAAAAAAAAMAAABhLmIACAAMAAgABwAIAAAAAAAAASAAAAA=", + "empty": "/////wgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAACkAAAACAAAABAAAAAAAAAAAwAAAGEuYgADAAAAYAAAADQAAAAEAAAAtP///xQAAAAEAAAABwAAAGtlZXAgbWUADQAAAHByb3ZpZGVyLm5vdGUAAADg////EAAAAAQAAAACAAAAe30AAAQAAAB0YWdzAAAAAAgADAAEAAgACAAAABAAAAAEAAAAAAAAAAAAAAAHAAAAY29tbWVudAAIAAwACAAHAAgAAAAAAAABIAAAAAAAAAA=", + "literal": "/////5gBAAAQAAAAAAAKAA4ABgAFAAgACgAAAAABBAAQAAAAAAAKAAwAAAAEAAgACgAAADwAAAAEAAAAAQAAAAQAAAAA////GAAAAAQAAAAIAAAAcmV0YWluZWQAAAAACAAAAHByb3ZpZGVyAAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAAD8AAAACAAAABAAAAAAAAAAAwAAAGEuYgADAAAAmAAAADgAAAAEAAAAfP///xgAAAAEAAAACwAAAGtlZXAgbWUgdG9vAA0AAABwcm92aWRlci5ub3RlAAAArP///0QAAAAEAAAANQAAAHsiYS5iIjogImxpdGVyYWwgXCJ2YWx1ZVwiIiwgIiI6ICIiLCAi6ZuqIjogIlx1MDAwMCJ9AAAABAAAAHRhZ3MAAAAACAAMAAQACAAIAAAAMAAAAAQAAAAjAAAAQSBjYWbDqSBjb2x1bW4AIHdpdGggYSBsaXRlcmFsIGRvdC4ABwAAAGNvbW1lbnQACAAMAAgABwAIAAAAAAAAASAAAAA=", + "nested": "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" + }, + "invalid": { + "tags_duplicate_keys": "/////8gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABoAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAACgAAAAEAAAAGQAAAHsia2V5Ijoib25lIiwia2V5IjoidHdvIn0AAAAEAAAAdGFncwAAAAAIAAwACAAHAAgAAAAAAAABIAAAAA==", + "tags_escaped_duplicate_keys": "/////9AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABsAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAACwAAAAEAAAAHgAAAHsia2V5Ijoib25lIiwiXHUwMDZiZXkiOiJ0d28ifQAABAAAAHRhZ3MAAAAACAAMAAgABwAIAAAAAAAAASAAAAAAAAAA", + "tags_array": "/////7AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABQAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABAAAAAEAAAAAgAAAFtdAAAEAAAAdGFncwAAAAAIAAwACAAHAAgAAAAAAAABIAAAAA==", + "tags_null": "/////7gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABUAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABQAAAAEAAAABAAAAG51bGwAAAAABAAAAHRhZ3MAAAAACAAMAAgABwAIAAAAAAAAASAAAAAAAAAA", + "tags_number": "/////7AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABQAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABAAAAAEAAAAAQAAADEAAAAEAAAAdGFncwAAAAAIAAwACAAHAAgAAAAAAAABIAAAAA==", + "tags_string": "/////7gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABUAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABQAAAAEAAAABgAAACJ0ZXh0IgAABAAAAHRhZ3MAAAAACAAMAAgABwAIAAAAAAAAASAAAAAAAAAA", + "tags_number_value": "/////7gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABYAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABgAAAAEAAAACQAAAHsia2V5IjoxfQAAAAQAAAB0YWdzAAAAAAgADAAIAAcACAAAAAAAAAEgAAAA", + "tags_null_value": "/////8AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABcAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABwAAAAEAAAADAAAAHsia2V5IjpudWxsfQAAAAAEAAAAdGFncwAAAAAIAAwACAAHAAgAAAAAAAABIAAAAAAAAAA=", + "tags_boolean_value": "/////8AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABcAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABwAAAAEAAAADAAAAHsia2V5Ijp0cnVlfQAAAAAEAAAAdGFncwAAAAAIAAwACAAHAAgAAAAAAAABIAAAAAAAAAA=", + "tags_object_value": "/////7gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABYAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABgAAAAEAAAACgAAAHsia2V5Ijp7fX0AAAQAAAB0YWdzAAAAAAgADAAIAAcACAAAAAAAAAEgAAAA", + "tags_array_value": "/////7gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABYAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABgAAAAEAAAACgAAAHsia2V5IjpbXX0AAAQAAAB0YWdzAAAAAAgADAAIAAcACAAAAAAAAAEgAAAA", + "tags_malformed": "/////7gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABUAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABQAAAAEAAAABwAAAHsia2V5IjoABAAAAHRhZ3MAAAAACAAMAAgABwAIAAAAAAAAASAAAAAAAAAA", + "tags_trailing": "/////7gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABYAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABgAAAAEAAAACwAAAHt9IHRyYWlsaW5nAAQAAAB0YWdzAAAAAAgADAAIAAcACAAAAAAAAAEgAAAA", + "tags_invalid_utf8": "/////7gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABYAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABgAAAAEAAAACwAAAHsia2V5Ijoi/yJ9AAQAAAB0YWdzAAAAAAgADAAIAAcACAAAAAAAAAEgAAAA", + "tags_surrogate_key": "/////8AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABgAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAACAAAAAEAAAAEgAAAHsiXHVkODAwIjoidmFsdWUifQAABAAAAHRhZ3MAAAAACAAMAAgABwAIAAAAAAAAASAAAAA=", + "tags_surrogate_value": "/////8AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABgAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAACAAAAAEAAAAEAAAAHsia2V5IjoiXHVkZmZmIn0AAAAABAAAAHRhZ3MAAAAACAAMAAgABwAIAAAAAAAAASAAAAA=", + "comment_invalid_utf8": "/////7AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABQAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABAAAAAEAAAAAQAAAP8AAAAHAAAAY29tbWVudAAIAAwACAAHAAgAAAAAAAABIAAAAA==", + "comment_overlong_utf8": "/////7AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABQAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABAAAAAEAAAAAgAAAMCvAAAHAAAAY29tbWVudAAIAAwACAAHAAgAAAAAAAABIAAAAA==", + "comment_surrogate_utf8": "/////7AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABQAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABAAAAAEAAAAAwAAAO2ggAAHAAAAY29tbWVudAAIAAwACAAHAAgAAAAAAAABIAAAAA==", + "comment_truncated_utf8": "/////7AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAABQAAAACAAAABAAAAAAAAAAAwAAAGEuYgABAAAADAAAAAgADAAEAAgACAAAABAAAAAEAAAAAgAAAOKCAAAHAAAAY29tbWVudAAIAAwACAAHAAgAAAAAAAABIAAAAA==", + "duplicate_arrow_comment": "/////9gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAAB0AAAACAAAABAAAAAAAAAAAwAAAGEuYgACAAAAMAAAAAQAAADg////EAAAAAQAAAAAAAAAAAAAAAcAAABjb21tZW50AAgADAAEAAgACAAAABAAAAAEAAAAAAAAAAAAAAAHAAAAY29tbWVudAAIAAwACAAHAAgAAAAAAAABIAAAAAAAAAA=", + "duplicate_arrow_tags": "/////9gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAAB0AAAACAAAABAAAAAAAAAAAwAAAGEuYgACAAAAMAAAAAQAAADg////EAAAAAQAAAACAAAAe30AAAQAAAB0YWdzAAAAAAgADAAEAAgACAAAABAAAAAEAAAAAgAAAHt9AAAEAAAAdGFncwAAAAAIAAwACAAHAAgAAAAAAAABIAAAAAAAAAA=", + "duplicate_arrow_opaque": "/////+gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABAhQAAACEAAAACAAAABAAAAAAAAAAAwAAAGEuYgACAAAAOAAAAAQAAADY////FAAAAAQAAAAEAAAAa2VlcAAAAAAIAAAAcHJvdmlkZXIAAAAACAAMAAQACAAIAAAAFAAAAAQAAAAEAAAAa2VlcAAAAAAIAAAAcHJvdmlkZXIAAAAACAAMAAgABwAIAAAAAAAAASAAAAAAAAAA", + "nested_struct": "/////wABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAENFAAAACAAAAAEAAAAAQAAACwAAAAFAAAAb3V0ZXIAAAAEAAYABAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQUUAAAAaAAAAAgAAAAUAAAAAAAAAAUAAABjaGlsZAAAAAEAAAAMAAAACAAMAAQACAAIAAAAKAAAAAQAAAAZAAAAeyJrZXkiOiJvbmUiLCJrZXkiOiJ0d28ifQAAAAQAAAB0YWdzAAAAAAQABAAEAAAA", + "nested_list": "/////wABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEMFAAAACAAAAAEAAAAAQAAACwAAAAFAAAAb3V0ZXIAAAAEAAYABAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQUUAAAAaAAAAAgAAAAUAAAAAAAAAAUAAABjaGlsZAAAAAEAAAAMAAAACAAMAAQACAAIAAAAKAAAAAQAAAAZAAAAeyJrZXkiOiJvbmUiLCJrZXkiOiJ0d28ifQAAAAQAAAB0YWdzAAAAAAQABAAEAAAA", + "nested_map": "/////2gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAERFAAAABwAAAAEAAAAAQAAABQAAAAFAAAAb3V0ZXIAAAAQ////RP///wAAAA0YAAAAJAAAAAQAAAACAAAAtAAAACwAAAAHAAAAZW50cmllcwAEAAYABAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQUUAAAAZAAAAAgAAAAUAAAAAAAAAAUAAABjaGlsZAAAAAEAAAAMAAAACAAMAAQACAAIAAAAKAAAAAQAAAAZAAAAeyJrZXkiOiJvbmUiLCJrZXkiOiJ0d28ifQAAAAQAAAB0YWdzAAAAAMz///8QABQACAAAAAcADAAAABAAEAAAAAAAAAUQAAAAGAAAAAQAAAAAAAAAAwAAAGtleQAEAAQABAAAAAAAAAA=", + "nested_dictionary": "/////yABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAEAAAAtP///wQAAAABAAAAFAAAABAAGAAIAAYABwAMABAAFAAQAAAAAAABDRgAAABIAAAAJAAAAAQAAAABAAAAUAAAAAUAAABvdXRlcgAAAAgACAAAAAQACAAAAAwAAAAIAAwACAAHAAgAAAAAAAABIAAAAAQABgAEAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAABoAAAACAAAABQAAAAAAAAABQAAAGNoaWxkAAAAAQAAAAwAAAAIAAwABAAIAAgAAAAoAAAABAAAABkAAAB7ImtleSI6Im9uZSIsImtleSI6InR3byJ9AAAABAAAAHRhZ3MAAAAABAAEAAQAAAA=" + } + }, + "generated_column_schemas": { + "owner_schema_path": [ + "main" + ], + "valid": { + "ordinary": "/////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAABwAAAAEAAAAAAAAAAEAAABuAAAACAAMAAgABwAIAAAAAAAAAUAAAAA=", + "interleaved_chain": "/////7gRAAAQAAAAAAAKAA4ABgAFAAgACgAAAAABBAAQAAAAAAAKAAwAAAAEAAgACgAAADwAAAAEAAAAAQAAAAQAAACI9v//GAAAAAQAAAAIAAAAcmV0YWluZWQAAAAACAAAAHByb3ZpZGVyAAAAAAQAAAAUEQAAgAgAADAIAAAEAAAAnvf//wAAAQIUAAAAEAgAAAgAAAAUAAAAAAAAAAQAAABsYXN0AAAAAAIAAABAAAAABAAAAPz2//8UAAAABAAAAAcAAAB2aXJ0dWFsABgAAAB2Z2kuY29sdW1uLmdlbmVyYXRpb24udjEAAAAANPf//5AHAAAEAAAAgAcAAC8vLy8vMGdDQUFBUUFBQUFBQUFLQUF3QUJnQUZBQWdBQ2dBQUFBQUJCQUFNQUFBQUNBQUlBQUFBQkFBSUFBQUFCQUFBQUFRQUFBRHdBUUFBdEFFQUFFZ0JBQUFFQUFBQU1QNy8vd0FBQUEwa0FBQUFPQUFBQUFRQUFBQUZBQUFBL0FBQUFNd0FBQUNZQUFBQVlBQUFBQ0FBQUFBU0FBQUFaWFpoYkhWaGRHbHZibDlqYjI1MFpYaDBBQUJFL3YvL2VQNy8vd0FBQUFZUUFBQUFMQUFBQUFRQUFBQUFBQUFBR0FBQUFIQnlaWE5sY25abFgybHVjMlZ5ZEdsdmJsOXZjbVJsY2dBQUFBQ0Evdi8vdFA3Ly93QUFBQVVRQUFBQUpBQUFBQVFBQUFBQUFBQUFFZ0FBQUdSbFptRjFiSFJmYm5Wc2JGOXZjbVJsY2dBQXRQNy8vK2orLy84QUFBQUZFQUFBQUNBQUFBQUVBQUFBQUFBQUFBMEFBQUJrWldaaGRXeDBYMjl5WkdWeUFBQUE1UDcvL3hqLy8vOEFBQUFGRUFBQUFCd0FBQUFFQUFBQUFBQUFBQWtBQUFCamIyeHNZWFJwYjI0QUFBQVEvLy8vUlAvLy93QUFBQVVRQUFBQUhBQUFBQVFBQUFBQUFBQUFDQUFBQUhScGJXVjZiMjVsQUFBQUFEei8vLzl3Ly8vL0FBQUFEQlFBQUFBZ0FBQUFCQUFBQUFFQUFBQW9BQUFBQ3dBQUFITmphR1Z0WVY5d1lYUm9BR3ovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFCQUFBQUdsMFpXMEFBQUFBcFAvLy85ai8vLzhBQUFBRkVBQUFBQmdBQUFBRUFBQUFBQUFBQUFjQUFBQndjbTltYVd4bEFNei8vLzhRQUJRQUNBQUFBQWNBREFBQUFCQUFFQUFBQUFBQUFBVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUF3QUFBSE54YkFBRUFBUUFCQUFBQVAvLy8vK1lBZ0FBRkFBQUFBQUFBQUFNQUJZQUJnQUZBQWdBREFBTUFBQUFBQU1FQUJnQUFBQ29BQUFBQUFBQUFBQUFDZ0FZQUF3QUJBQUlBQW9BQUFDOEFRQUFFQUFBQUFFQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBZ0FBQUFBQUFBQURnQUFBQUFBQUFBWUFBQUFBQUFBQUFBQUFBQUFBQUFBR0FBQUFBQUFBQUFJQUFBQUFBQUFBQ0FBQUFBQUFBQUFId0FBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBSUFBQUFBQUFBQUVnQUFBQUFBQUFBQUFBQUFBQUFBQUJJQUFBQUFBQUFBQWdBQUFBQUFBQUFVQUFBQUFBQUFBQUVBQUFBQUFBQUFGZ0FBQUFBQUFBQUFBQUFBQUFBQUFCWUFBQUFBQUFBQUFBQUFBQUFBQUFBV0FBQUFBQUFBQUFJQUFBQUFBQUFBR0FBQUFBQUFBQUFBd0FBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQWFBQUFBQUFBQUFBSUFBQUFBQUFBQUhBQUFBQUFBQUFBQmdBQUFBQUFBQUI0QUFBQUFBQUFBQUFBQUFBQUFBQUFlQUFBQUFBQUFBQUlBQUFBQUFBQUFJQUFBQUFBQUFBQUF3QUFBQUFBQUFDSUFBQUFBQUFBQUFBQUFBQUFBQUFBaUFBQUFBQUFBQUFJQUFBQUFBQUFBSkFBQUFBQUFBQUFDZ0FBQUFBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFBQW9BQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUtBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT0FBQUFaRzkxWW14bFpDQXJJSFJoYVd3QUFBQUFBQUFmQUFBQWRtZHBMbVY0Y0hKbGMzTnBiMjR1WkhWamEyUmlMbk5qWVd4aGNpNTJNUUFBQUFBQUFRQUFBQUFBQUFBRUFBQUFiV0ZwYmdBQUFBQUFBQUFBQXdBQUFGVlVRd0FBQUFBQUFBQUFBQVlBQUFCaWFXNWhjbmtBQUFBQUFBQURBQUFBWVhOakFBQUFBQUFBQUFBQUNnQUFBRzUxYkd4elgyeGhjM1FBQUFBQUFBQUJBQUFBQUFBQUFQLy8vLzhBQUFBQQAAAAAXAAAAdmdpLmNvbHVtbi5nZW5lcmF0ZWQudjEA+Pb//wAAAAFAAAAANPf//wAAAQIQAAAAIAAAAAQAAAAAAAAABAAAAHRhaWwAAAAACAAOAAgABwAIAAAAAAAAAUAAAAAAABIAGAAIAAYABwAMAAAAEAAUABIAAAAAAAECFAAAAGgIAAAIAAAAFAAAAAAAAAAHAAAAZG91YmxlZAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAACIBwAABAAAAHgHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNnQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFFQUFBQUFBQUFBRkFBQUFBQUFBQUFBQUFBQUFBQUFBQlFBQUFBQUFBQUFBQUFBQUFBQUFBQVVBQUFBQUFBQUFBSUFBQUFBQUFBQUZnQUFBQUFBQUFBQXdBQUFBQUFBQUJnQUFBQUFBQUFBQUFBQUFBQUFBQUFZQUFBQUFBQUFBQUlBQUFBQUFBQUFHZ0FBQUFBQUFBQUJnQUFBQUFBQUFCd0FBQUFBQUFBQUFBQUFBQUFBQUFBY0FBQUFBQUFBQUFJQUFBQUFBQUFBSGdBQUFBQUFBQUFBd0FBQUFBQUFBQ0FBQUFBQUFBQUFBQUFBQUFBQUFBQWdBQUFBQUFBQUFBSUFBQUFBQUFBQUlnQUFBQUFBQUFBQ2dBQUFBQUFBQUNZQUFBQUFBQUFBQUFBQUFBQUFBQUFtQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFCQUFBQUcxaGFXNEFBQUFBQUFBQUFBTUFBQUJWVkVNQUFBQUFBQUFBQUFBR0FBQUFZbWx1WVhKNUFBQUFBQUFBQXdBQUFHRnpZd0FBQUFBQUFBQUFBQW9BQUFCdWRXeHNjMTlzWVhOMEFBQUFBQUFBQVFBQUFBQUFBQUQvLy8vL0FBQUFBQT09AAAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQDE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAAAAAAA=", + "literal_dotted_column": "/////8gIAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAABgCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAHAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAACAAAASAAAAAQAAADI////FAAAAAQAAAAHAAAAdmlydHVhbAAYAAAAdmdpLmNvbHVtbi5nZW5lcmF0aW9uLnYxAAAAAAgADAAEAAgACAAAAJAHAAAEAAAAgAcAAC8vLy8vMGdDQUFBUUFBQUFBQUFLQUF3QUJnQUZBQWdBQ2dBQUFBQUJCQUFNQUFBQUNBQUlBQUFBQkFBSUFBQUFCQUFBQUFRQUFBRHdBUUFBdEFFQUFFZ0JBQUFFQUFBQU1QNy8vd0FBQUEwa0FBQUFPQUFBQUFRQUFBQUZBQUFBL0FBQUFNd0FBQUNZQUFBQVlBQUFBQ0FBQUFBU0FBQUFaWFpoYkhWaGRHbHZibDlqYjI1MFpYaDBBQUJFL3YvL2VQNy8vd0FBQUFZUUFBQUFMQUFBQUFRQUFBQUFBQUFBR0FBQUFIQnlaWE5sY25abFgybHVjMlZ5ZEdsdmJsOXZjbVJsY2dBQUFBQ0Evdi8vdFA3Ly93QUFBQVVRQUFBQUpBQUFBQVFBQUFBQUFBQUFFZ0FBQUdSbFptRjFiSFJmYm5Wc2JGOXZjbVJsY2dBQXRQNy8vK2orLy84QUFBQUZFQUFBQUNBQUFBQUVBQUFBQUFBQUFBMEFBQUJrWldaaGRXeDBYMjl5WkdWeUFBQUE1UDcvL3hqLy8vOEFBQUFGRUFBQUFCd0FBQUFFQUFBQUFBQUFBQWtBQUFCamIyeHNZWFJwYjI0QUFBQVEvLy8vUlAvLy93QUFBQVVRQUFBQUhBQUFBQVFBQUFBQUFBQUFDQUFBQUhScGJXVjZiMjVsQUFBQUFEei8vLzl3Ly8vL0FBQUFEQlFBQUFBZ0FBQUFCQUFBQUFFQUFBQW9BQUFBQ3dBQUFITmphR1Z0WVY5d1lYUm9BR3ovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFCQUFBQUdsMFpXMEFBQUFBcFAvLy85ai8vLzhBQUFBRkVBQUFBQmdBQUFBRUFBQUFBQUFBQUFjQUFBQndjbTltYVd4bEFNei8vLzhRQUJRQUNBQUFBQWNBREFBQUFCQUFFQUFBQUFBQUFBVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUF3QUFBSE54YkFBRUFBUUFCQUFBQVAvLy8vK1lBZ0FBRkFBQUFBQUFBQUFNQUJZQUJnQUZBQWdBREFBTUFBQUFBQU1FQUJnQUFBQ29BQUFBQUFBQUFBQUFDZ0FZQUF3QUJBQUlBQW9BQUFDOEFRQUFFQUFBQUFFQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBZ0FBQUFBQUFBQUNRQUFBQUFBQUFBWUFBQUFBQUFBQUFBQUFBQUFBQUFBR0FBQUFBQUFBQUFJQUFBQUFBQUFBQ0FBQUFBQUFBQUFId0FBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBSUFBQUFBQUFBQUVnQUFBQUFBQUFBQUFBQUFBQUFBQUJJQUFBQUFBQUFBQWdBQUFBQUFBQUFVQUFBQUFBQUFBQUVBQUFBQUFBQUFGZ0FBQUFBQUFBQUFBQUFBQUFBQUFCWUFBQUFBQUFBQUFBQUFBQUFBQUFBV0FBQUFBQUFBQUFJQUFBQUFBQUFBR0FBQUFBQUFBQUFBd0FBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQWFBQUFBQUFBQUFBSUFBQUFBQUFBQUhBQUFBQUFBQUFBQmdBQUFBQUFBQUI0QUFBQUFBQUFBQUFBQUFBQUFBQUFlQUFBQUFBQUFBQUlBQUFBQUFBQUFJQUFBQUFBQUFBQUF3QUFBQUFBQUFDSUFBQUFBQUFBQUFBQUFBQUFBQUFBaUFBQUFBQUFBQUFJQUFBQUFBQUFBSkFBQUFBQUFBQUFDZ0FBQUFBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFBQW9BQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUtBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBSkFBQUFJbUV1WWlJZ0tpQXlBQUFBQUFBQUFBQUFBQUFmQUFBQWRtZHBMbVY0Y0hKbGMzTnBiMjR1WkhWamEyUmlMbk5qWVd4aGNpNTJNUUFBQUFBQUFRQUFBQUFBQUFBRUFBQUFiV0ZwYmdBQUFBQUFBQUFBQXdBQUFGVlVRd0FBQUFBQUFBQUFBQVlBQUFCaWFXNWhjbmtBQUFBQUFBQURBQUFBWVhOakFBQUFBQUFBQUFBQUNnQUFBRzUxYkd4elgyeGhjM1FBQUFBQUFBQUJBQUFBQUFBQUFQLy8vLzhBQUFBQQAAAAAXAAAAdmdpLmNvbHVtbi5nZW5lcmF0ZWQudjEAxP///wAAAAFAAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAABwAAAAEAAAAAAAAAAMAAABhLmIACAAMAAgABwAIAAAAAAAAAUAAAAAAAAAA" + }, + "invalid": { + "missing_generation": "/////4AIAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAAAcCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAA2AcAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAABAAAADAAAAAgADAAEAAgACAAAAIgHAAAEAAAAeAcAAC8vLy8vMGdDQUFBUUFBQUFBQUFLQUF3QUJnQUZBQWdBQ2dBQUFBQUJCQUFNQUFBQUNBQUlBQUFBQkFBSUFBQUFCQUFBQUFRQUFBRHdBUUFBdEFFQUFFZ0JBQUFFQUFBQU1QNy8vd0FBQUEwa0FBQUFPQUFBQUFRQUFBQUZBQUFBL0FBQUFNd0FBQUNZQUFBQVlBQUFBQ0FBQUFBU0FBQUFaWFpoYkhWaGRHbHZibDlqYjI1MFpYaDBBQUJFL3YvL2VQNy8vd0FBQUFZUUFBQUFMQUFBQUFRQUFBQUFBQUFBR0FBQUFIQnlaWE5sY25abFgybHVjMlZ5ZEdsdmJsOXZjbVJsY2dBQUFBQ0Evdi8vdFA3Ly93QUFBQVVRQUFBQUpBQUFBQVFBQUFBQUFBQUFFZ0FBQUdSbFptRjFiSFJmYm5Wc2JGOXZjbVJsY2dBQXRQNy8vK2orLy84QUFBQUZFQUFBQUNBQUFBQUVBQUFBQUFBQUFBMEFBQUJrWldaaGRXeDBYMjl5WkdWeUFBQUE1UDcvL3hqLy8vOEFBQUFGRUFBQUFCd0FBQUFFQUFBQUFBQUFBQWtBQUFCamIyeHNZWFJwYjI0QUFBQVEvLy8vUlAvLy93QUFBQVVRQUFBQUhBQUFBQVFBQUFBQUFBQUFDQUFBQUhScGJXVjZiMjVsQUFBQUFEei8vLzl3Ly8vL0FBQUFEQlFBQUFBZ0FBQUFCQUFBQUFFQUFBQW9BQUFBQ3dBQUFITmphR1Z0WVY5d1lYUm9BR3ovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFCQUFBQUdsMFpXMEFBQUFBcFAvLy85ai8vLzhBQUFBRkVBQUFBQmdBQUFBRUFBQUFBQUFBQUFjQUFBQndjbTltYVd4bEFNei8vLzhRQUJRQUNBQUFBQWNBREFBQUFCQUFFQUFBQUFBQUFBVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUF3QUFBSE54YkFBRUFBUUFCQUFBQVAvLy8vK1lBZ0FBRkFBQUFBQUFBQUFNQUJZQUJnQUZBQWdBREFBTUFBQUFBQU1FQUJnQUFBQ2dBQUFBQUFBQUFBQUFDZ0FZQUF3QUJBQUlBQW9BQUFDOEFRQUFFQUFBQUFFQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBZ0FBQUFBQUFBQUJRQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFJQUFBQUFBQUFBQmdBQUFBQUFBQUFId0FBQUFBQUFBQTRBQUFBQUFBQUFBQUFBQUFBQUFBQU9BQUFBQUFBQUFBSUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQWdBQUFBQUFBQUFTQUFBQUFBQUFBQUVBQUFBQUFBQUFGQUFBQUFBQUFBQUFBQUFBQUFBQUFCUUFBQUFBQUFBQUFBQUFBQUFBQUFBVUFBQUFBQUFBQUFJQUFBQUFBQUFBRmdBQUFBQUFBQUFBd0FBQUFBQUFBQmdBQUFBQUFBQUFBQUFBQUFBQUFBQVlBQUFBQUFBQUFBSUFBQUFBQUFBQUdnQUFBQUFBQUFBQmdBQUFBQUFBQUJ3QUFBQUFBQUFBQUFBQUFBQUFBQUFjQUFBQUFBQUFBQUlBQUFBQUFBQUFIZ0FBQUFBQUFBQUF3QUFBQUFBQUFDQUFBQUFBQUFBQUFBQUFBQUFBQUFBZ0FBQUFBQUFBQUFJQUFBQUFBQUFBSWdBQUFBQUFBQUFDZ0FBQUFBQUFBQ1lBQUFBQUFBQUFBQUFBQUFBQUFBQW1BQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUtBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBRkFBQUFiaUFxSURJQUFBQUFBQUFBSHdBQUFIWm5hUzVsZUhCeVpYTnphVzl1TG1SMVkydGtZaTV6WTJGc1lYSXVkakVBQUFBQUFBRUFBQUFBQUFBQUJBQUFBRzFoYVc0QUFBQUFBQUFBQUFNQUFBQlZWRU1BQUFBQUFBQUFBQUFHQUFBQVltbHVZWEo1QUFBQUFBQUFBd0FBQUdGell3QUFBQUFBQUFBQUFBb0FBQUJ1ZFd4c2MxOXNZWE4wQUFBQUFBQUFBUUFBQUFBQUFBRC8vLy8vQUFBQUFBPT0AAAAAFwAAAHZnaS5jb2x1bW4uZ2VuZXJhdGVkLnYxAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAA", + "missing_expression": "/////xABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACsAAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAaAAAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAABAAAADAAAAAgADAAEAAgACAAAABQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAADE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAA==", + "unknown_generation": "/////xgJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACwCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAbAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHVua25vd24AGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAACIBwAABAAAAHgHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNnQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFFQUFBQUFBQUFBRkFBQUFBQUFBQUFBQUFBQUFBQUFBQlFBQUFBQUFBQUFBQUFBQUFBQUFBQVVBQUFBQUFBQUFBSUFBQUFBQUFBQUZnQUFBQUFBQUFBQXdBQUFBQUFBQUJnQUFBQUFBQUFBQUFBQUFBQUFBQUFZQUFBQUFBQUFBQUlBQUFBQUFBQUFHZ0FBQUFBQUFBQUJnQUFBQUFBQUFCd0FBQUFBQUFBQUFBQUFBQUFBQUFBY0FBQUFBQUFBQUFJQUFBQUFBQUFBSGdBQUFBQUFBQUFBd0FBQUFBQUFBQ0FBQUFBQUFBQUFBQUFBQUFBQUFBQWdBQUFBQUFBQUFBSUFBQUFBQUFBQUlnQUFBQUFBQUFBQ2dBQUFBQUFBQUNZQUFBQUFBQUFBQUFBQUFBQUFBQUFtQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFCQUFBQUcxaGFXNEFBQUFBQUFBQUFBTUFBQUJWVkVNQUFBQUFBQUFBQUFBR0FBQUFZbWx1WVhKNUFBQUFBQUFBQXdBQUFHRnpZd0FBQUFBQUFBQUFBQW9BQUFCdWRXeHNjMTlzWVhOMEFBQUFBQUFBQVFBQUFBQUFBQUQvLy8vL0FBQUFBQT09AAAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQDE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAAAAAAA=", + "stored_unsupported": "/////xgJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACwCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAbAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABgAAAHN0b3JlZAAAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAACIBwAABAAAAHgHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNnQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFFQUFBQUFBQUFBRkFBQUFBQUFBQUFBQUFBQUFBQUFBQlFBQUFBQUFBQUFBQUFBQUFBQUFBQVVBQUFBQUFBQUFBSUFBQUFBQUFBQUZnQUFBQUFBQUFBQXdBQUFBQUFBQUJnQUFBQUFBQUFBQUFBQUFBQUFBQUFZQUFBQUFBQUFBQUlBQUFBQUFBQUFHZ0FBQUFBQUFBQUJnQUFBQUFBQUFCd0FBQUFBQUFBQUFBQUFBQUFBQUFBY0FBQUFBQUFBQUFJQUFBQUFBQUFBSGdBQUFBQUFBQUFBd0FBQUFBQUFBQ0FBQUFBQUFBQUFBQUFBQUFBQUFBQWdBQUFBQUFBQUFBSUFBQUFBQUFBQUlnQUFBQUFBQUFBQ2dBQUFBQUFBQUNZQUFBQUFBQUFBQUFBQUFBQUFBQUFtQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFCQUFBQUcxaGFXNEFBQUFBQUFBQUFBTUFBQUJWVkVNQUFBQUFBQUFBQUFBR0FBQUFZbWx1WVhKNUFBQUFBQUFBQXdBQUFHRnpZd0FBQUFBQUFBQUFBQW9BQUFCdWRXeHNjMTlzWVhOMEFBQUFBQUFBQVFBQUFBQUFBQUQvLy8vL0FBQUFBQT09AAAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQDE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAAAAAAA=", + "default_conflict": "/////8AQAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAABcEAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAGBAAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAFAAAATAgAAAgIAADkBwAAsAcAAAQAAADQ9///iAcAAAQAAAB4BwAALy8vLy8wZ0NBQUFRQUFBQUFBQUtBQXdBQmdBRkFBZ0FDZ0FBQUFBQkJBQU1BQUFBQ0FBSUFBQUFCQUFJQUFBQUJBQUFBQVFBQUFEd0FRQUF0QUVBQUVnQkFBQUVBQUFBTVA3Ly93QUFBQTBrQUFBQU9BQUFBQVFBQUFBRkFBQUEvQUFBQU13QUFBQ1lBQUFBWUFBQUFDQUFBQUFTQUFBQVpYWmhiSFZoZEdsdmJsOWpiMjUwWlhoMEFBQkUvdi8vZVA3Ly93QUFBQVlRQUFBQUxBQUFBQVFBQUFBQUFBQUFHQUFBQUhCeVpYTmxjblpsWDJsdWMyVnlkR2x2Ymw5dmNtUmxjZ0FBQUFDQS92Ly90UDcvL3dBQUFBVVFBQUFBSkFBQUFBUUFBQUFBQUFBQUVnQUFBR1JsWm1GMWJIUmZiblZzYkY5dmNtUmxjZ0FBdFA3Ly8raisvLzhBQUFBRkVBQUFBQ0FBQUFBRUFBQUFBQUFBQUEwQUFBQmtaV1poZFd4MFgyOXlaR1Z5QUFBQTVQNy8veGovLy84QUFBQUZFQUFBQUJ3QUFBQUVBQUFBQUFBQUFBa0FBQUJqYjJ4c1lYUnBiMjRBQUFBUS8vLy9SUC8vL3dBQUFBVVFBQUFBSEFBQUFBUUFBQUFBQUFBQUNBQUFBSFJwYldWNmIyNWxBQUFBQUR6Ly8vOXcvLy8vQUFBQURCUUFBQUFnQUFBQUJBQUFBQUVBQUFBb0FBQUFDd0FBQUhOamFHVnRZVjl3WVhSb0FHei8vLzhRQUJRQUNBQUdBQWNBREFBQUFCQUFFQUFBQUFBQUFRVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUJBQUFBR2wwWlcwQUFBQUFwUC8vLzlqLy8vOEFBQUFGRUFBQUFCZ0FBQUFFQUFBQUFBQUFBQWNBQUFCd2NtOW1hV3hsQU16Ly8vOFFBQlFBQ0FBQUFBY0FEQUFBQUJBQUVBQUFBQUFBQUFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQXdBQUFITnhiQUFFQUFRQUJBQUFBUC8vLy8rWUFnQUFGQUFBQUFBQUFBQU1BQllBQmdBRkFBZ0FEQUFNQUFBQUFBTUVBQmdBQUFDZ0FBQUFBQUFBQUFBQUNnQVlBQXdBQkFBSUFBb0FBQUM4QVFBQUVBQUFBQUVBQUFBQUFBQUFBQUFBQUJvQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBSUFBQUFBQUFBQUFnQUFBQUFBQUFBQlFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUlBQUFBQUFBQUFCZ0FBQUFBQUFBQUh3QUFBQUFBQUFBNEFBQUFBQUFBQUFBQUFBQUFBQUFBT0FBQUFBQUFBQUFJQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBZ0FBQUFBQUFBQVNBQUFBQUFBQUFBRUFBQUFBQUFBQUZBQUFBQUFBQUFBQUFBQUFBQUFBQUJRQUFBQUFBQUFBQUFBQUFBQUFBQUFVQUFBQUFBQUFBQUlBQUFBQUFBQUFGZ0FBQUFBQUFBQUF3QUFBQUFBQUFCZ0FBQUFBQUFBQUFBQUFBQUFBQUFBWUFBQUFBQUFBQUFJQUFBQUFBQUFBR2dBQUFBQUFBQUFCZ0FBQUFBQUFBQndBQUFBQUFBQUFBQUFBQUFBQUFBQWNBQUFBQUFBQUFBSUFBQUFBQUFBQUhnQUFBQUFBQUFBQXdBQUFBQUFBQUNBQUFBQUFBQUFBQUFBQUFBQUFBQUFnQUFBQUFBQUFBQUlBQUFBQUFBQUFJZ0FBQUFBQUFBQUNnQUFBQUFBQUFDWUFBQUFBQUFBQUFBQUFBQUFBQUFBbUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBS0FBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFGQUFBQWJpQXFJRElBQUFBQUFBQUFId0FBQUhabmFTNWxlSEJ5WlhOemFXOXVMbVIxWTJ0a1lpNXpZMkZzWVhJdWRqRUFBQUFBQUFFQUFBQUFBQUFBQkFBQUFHMWhhVzRBQUFBQUFBQUFBQU1BQUFCVlZFTUFBQUFBQUFBQUFBQUdBQUFBWW1sdVlYSjVBQUFBQUFBQUF3QUFBR0Z6WXdBQUFBQUFBQUFBQUFvQUFBQnVkV3hzYzE5c1lYTjBBQUFBQUFBQUFRQUFBQUFBQUFELy8vLy9BQUFBQUE9PQAAAAAVAAAAdmdpLmNvbHVtbi5kZWZhdWx0LnYxAAAAeP///xgAAAAEAAAACAAAAHJldGFpbmVkAAAAAA0AAABwcm92aWRlci5ub3RlAAAAqP///xAAAAAEAAAAAAAAAAAAAAAHAAAAY29tbWVudADI////FAAAAAQAAAAHAAAAdmlydHVhbAAYAAAAdmdpLmNvbHVtbi5nZW5lcmF0aW9uLnYxAAAAAAgADAAEAAgACAAAAIgHAAAEAAAAeAcAAC8vLy8vMGdDQUFBUUFBQUFBQUFLQUF3QUJnQUZBQWdBQ2dBQUFBQUJCQUFNQUFBQUNBQUlBQUFBQkFBSUFBQUFCQUFBQUFRQUFBRHdBUUFBdEFFQUFFZ0JBQUFFQUFBQU1QNy8vd0FBQUEwa0FBQUFPQUFBQUFRQUFBQUZBQUFBL0FBQUFNd0FBQUNZQUFBQVlBQUFBQ0FBQUFBU0FBQUFaWFpoYkhWaGRHbHZibDlqYjI1MFpYaDBBQUJFL3YvL2VQNy8vd0FBQUFZUUFBQUFMQUFBQUFRQUFBQUFBQUFBR0FBQUFIQnlaWE5sY25abFgybHVjMlZ5ZEdsdmJsOXZjbVJsY2dBQUFBQ0Evdi8vdFA3Ly93QUFBQVVRQUFBQUpBQUFBQVFBQUFBQUFBQUFFZ0FBQUdSbFptRjFiSFJmYm5Wc2JGOXZjbVJsY2dBQXRQNy8vK2orLy84QUFBQUZFQUFBQUNBQUFBQUVBQUFBQUFBQUFBMEFBQUJrWldaaGRXeDBYMjl5WkdWeUFBQUE1UDcvL3hqLy8vOEFBQUFGRUFBQUFCd0FBQUFFQUFBQUFBQUFBQWtBQUFCamIyeHNZWFJwYjI0QUFBQVEvLy8vUlAvLy93QUFBQVVRQUFBQUhBQUFBQVFBQUFBQUFBQUFDQUFBQUhScGJXVjZiMjVsQUFBQUFEei8vLzl3Ly8vL0FBQUFEQlFBQUFBZ0FBQUFCQUFBQUFFQUFBQW9BQUFBQ3dBQUFITmphR1Z0WVY5d1lYUm9BR3ovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFCQUFBQUdsMFpXMEFBQUFBcFAvLy85ai8vLzhBQUFBRkVBQUFBQmdBQUFBRUFBQUFBQUFBQUFjQUFBQndjbTltYVd4bEFNei8vLzhRQUJRQUNBQUFBQWNBREFBQUFCQUFFQUFBQUFBQUFBVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUF3QUFBSE54YkFBRUFBUUFCQUFBQVAvLy8vK1lBZ0FBRkFBQUFBQUFBQUFNQUJZQUJnQUZBQWdBREFBTUFBQUFBQU1FQUJnQUFBQ2dBQUFBQUFBQUFBQUFDZ0FZQUF3QUJBQUlBQW9BQUFDOEFRQUFFQUFBQUFFQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBZ0FBQUFBQUFBQUJRQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFJQUFBQUFBQUFBQmdBQUFBQUFBQUFId0FBQUFBQUFBQTRBQUFBQUFBQUFBQUFBQUFBQUFBQU9BQUFBQUFBQUFBSUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQWdBQUFBQUFBQUFTQUFBQUFBQUFBQUVBQUFBQUFBQUFGQUFBQUFBQUFBQUFBQUFBQUFBQUFCUUFBQUFBQUFBQUFBQUFBQUFBQUFBVUFBQUFBQUFBQUFJQUFBQUFBQUFBRmdBQUFBQUFBQUFBd0FBQUFBQUFBQmdBQUFBQUFBQUFBQUFBQUFBQUFBQVlBQUFBQUFBQUFBSUFBQUFBQUFBQUdnQUFBQUFBQUFBQmdBQUFBQUFBQUJ3QUFBQUFBQUFBQUFBQUFBQUFBQUFjQUFBQUFBQUFBQUlBQUFBQUFBQUFIZ0FBQUFBQUFBQUF3QUFBQUFBQUFDQUFBQUFBQUFBQUFBQUFBQUFBQUFBZ0FBQUFBQUFBQUFJQUFBQUFBQUFBSWdBQUFBQUFBQUFDZ0FBQUFBQUFBQ1lBQUFBQUFBQUFBQUFBQUFBQUFBQW1BQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUtBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBRkFBQUFiaUFxSURJQUFBQUFBQUFBSHdBQUFIWm5hUzVsZUhCeVpYTnphVzl1TG1SMVkydGtZaTV6WTJGc1lYSXVkakVBQUFBQUFBRUFBQUFBQUFBQUJBQUFBRzFoYVc0QUFBQUFBQUFBQUFNQUFBQlZWRU1BQUFBQUFBQUFBQUFHQUFBQVltbHVZWEo1QUFBQUFBQUFBd0FBQUdGell3QUFBQUFBQUFBQUFBb0FBQUJ1ZFd4c2MxOXNZWE4wQUFBQUFBQUFBUUFBQUFBQUFBRC8vLy8vQUFBQUFBPT0AAAAAFwAAAHZnaS5jb2x1bW4uZ2VuZXJhdGVkLnYxAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAA", + "legacy_conflict": "/////1AJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAADoCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAApAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAFAAAA2AAAAJQAAABwAAAAPAAAAAQAAABE////FAAAAAQAAAAFAAAAbiAqIDIAAAAUAAAAZ2VuZXJhdGVkX2V4cHJlc3Npb24AAAAAeP///xgAAAAEAAAACAAAAHJldGFpbmVkAAAAAA0AAABwcm92aWRlci5ub3RlAAAAqP///xAAAAAEAAAAAAAAAAAAAAAHAAAAY29tbWVudADI////FAAAAAQAAAAHAAAAdmlydHVhbAAYAAAAdmdpLmNvbHVtbi5nZW5lcmF0aW9uLnYxAAAAAAgADAAEAAgACAAAAIgHAAAEAAAAeAcAAC8vLy8vMGdDQUFBUUFBQUFBQUFLQUF3QUJnQUZBQWdBQ2dBQUFBQUJCQUFNQUFBQUNBQUlBQUFBQkFBSUFBQUFCQUFBQUFRQUFBRHdBUUFBdEFFQUFFZ0JBQUFFQUFBQU1QNy8vd0FBQUEwa0FBQUFPQUFBQUFRQUFBQUZBQUFBL0FBQUFNd0FBQUNZQUFBQVlBQUFBQ0FBQUFBU0FBQUFaWFpoYkhWaGRHbHZibDlqYjI1MFpYaDBBQUJFL3YvL2VQNy8vd0FBQUFZUUFBQUFMQUFBQUFRQUFBQUFBQUFBR0FBQUFIQnlaWE5sY25abFgybHVjMlZ5ZEdsdmJsOXZjbVJsY2dBQUFBQ0Evdi8vdFA3Ly93QUFBQVVRQUFBQUpBQUFBQVFBQUFBQUFBQUFFZ0FBQUdSbFptRjFiSFJmYm5Wc2JGOXZjbVJsY2dBQXRQNy8vK2orLy84QUFBQUZFQUFBQUNBQUFBQUVBQUFBQUFBQUFBMEFBQUJrWldaaGRXeDBYMjl5WkdWeUFBQUE1UDcvL3hqLy8vOEFBQUFGRUFBQUFCd0FBQUFFQUFBQUFBQUFBQWtBQUFCamIyeHNZWFJwYjI0QUFBQVEvLy8vUlAvLy93QUFBQVVRQUFBQUhBQUFBQVFBQUFBQUFBQUFDQUFBQUhScGJXVjZiMjVsQUFBQUFEei8vLzl3Ly8vL0FBQUFEQlFBQUFBZ0FBQUFCQUFBQUFFQUFBQW9BQUFBQ3dBQUFITmphR1Z0WVY5d1lYUm9BR3ovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFCQUFBQUdsMFpXMEFBQUFBcFAvLy85ai8vLzhBQUFBRkVBQUFBQmdBQUFBRUFBQUFBQUFBQUFjQUFBQndjbTltYVd4bEFNei8vLzhRQUJRQUNBQUFBQWNBREFBQUFCQUFFQUFBQUFBQUFBVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUF3QUFBSE54YkFBRUFBUUFCQUFBQVAvLy8vK1lBZ0FBRkFBQUFBQUFBQUFNQUJZQUJnQUZBQWdBREFBTUFBQUFBQU1FQUJnQUFBQ2dBQUFBQUFBQUFBQUFDZ0FZQUF3QUJBQUlBQW9BQUFDOEFRQUFFQUFBQUFFQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBZ0FBQUFBQUFBQUJRQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFJQUFBQUFBQUFBQmdBQUFBQUFBQUFId0FBQUFBQUFBQTRBQUFBQUFBQUFBQUFBQUFBQUFBQU9BQUFBQUFBQUFBSUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQWdBQUFBQUFBQUFTQUFBQUFBQUFBQUVBQUFBQUFBQUFGQUFBQUFBQUFBQUFBQUFBQUFBQUFCUUFBQUFBQUFBQUFBQUFBQUFBQUFBVUFBQUFBQUFBQUFJQUFBQUFBQUFBRmdBQUFBQUFBQUFBd0FBQUFBQUFBQmdBQUFBQUFBQUFBQUFBQUFBQUFBQVlBQUFBQUFBQUFBSUFBQUFBQUFBQUdnQUFBQUFBQUFBQmdBQUFBQUFBQUJ3QUFBQUFBQUFBQUFBQUFBQUFBQUFjQUFBQUFBQUFBQUlBQUFBQUFBQUFIZ0FBQUFBQUFBQUF3QUFBQUFBQUFDQUFBQUFBQUFBQUFBQUFBQUFBQUFBZ0FBQUFBQUFBQUFJQUFBQUFBQUFBSWdBQUFBQUFBQUFDZ0FBQUFBQUFBQ1lBQUFBQUFBQUFBQUFBQUFBQUFBQW1BQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUtBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBRkFBQUFiaUFxSURJQUFBQUFBQUFBSHdBQUFIWm5hUzVsZUhCeVpYTnphVzl1TG1SMVkydGtZaTV6WTJGc1lYSXVkakVBQUFBQUFBRUFBQUFBQUFBQUJBQUFBRzFoYVc0QUFBQUFBQUFBQUFNQUFBQlZWRU1BQUFBQUFBQUFBQUFHQUFBQVltbHVZWEo1QUFBQUFBQUFBd0FBQUdGell3QUFBQUFBQUFBQUFBb0FBQUJ1ZFd4c2MxOXNZWE4wQUFBQUFBQUFBUUFBQUFBQUFBRC8vLy8vQUFBQUFBPT0AAAAAFwAAAHZnaS5jb2x1bW4uZ2VuZXJhdGVkLnYxAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAAAAAAAA==", + "legacy_generated_only": "/////xABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACoAAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAZAAAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAABAAAADAAAAAgADAAEAAgACAAAABQAAAAEAAAABQAAAG4gKiAyAAAAFAAAAGdlbmVyYXRlZF9leHByZXNzaW9uAAAAAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAAAAAAAA==", + "unknown_metadata_version": "/////8AQAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAABcEAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAGBAAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAFAAAATAgAAAgIAADkBwAAsAcAAAQAAADQ9///iAcAAAQAAAB4BwAALy8vLy8wZ0NBQUFRQUFBQUFBQUtBQXdBQmdBRkFBZ0FDZ0FBQUFBQkJBQU1BQUFBQ0FBSUFBQUFCQUFJQUFBQUJBQUFBQVFBQUFEd0FRQUF0QUVBQUVnQkFBQUVBQUFBTVA3Ly93QUFBQTBrQUFBQU9BQUFBQVFBQUFBRkFBQUEvQUFBQU13QUFBQ1lBQUFBWUFBQUFDQUFBQUFTQUFBQVpYWmhiSFZoZEdsdmJsOWpiMjUwWlhoMEFBQkUvdi8vZVA3Ly93QUFBQVlRQUFBQUxBQUFBQVFBQUFBQUFBQUFHQUFBQUhCeVpYTmxjblpsWDJsdWMyVnlkR2x2Ymw5dmNtUmxjZ0FBQUFDQS92Ly90UDcvL3dBQUFBVVFBQUFBSkFBQUFBUUFBQUFBQUFBQUVnQUFBR1JsWm1GMWJIUmZiblZzYkY5dmNtUmxjZ0FBdFA3Ly8raisvLzhBQUFBRkVBQUFBQ0FBQUFBRUFBQUFBQUFBQUEwQUFBQmtaV1poZFd4MFgyOXlaR1Z5QUFBQTVQNy8veGovLy84QUFBQUZFQUFBQUJ3QUFBQUVBQUFBQUFBQUFBa0FBQUJqYjJ4c1lYUnBiMjRBQUFBUS8vLy9SUC8vL3dBQUFBVVFBQUFBSEFBQUFBUUFBQUFBQUFBQUNBQUFBSFJwYldWNmIyNWxBQUFBQUR6Ly8vOXcvLy8vQUFBQURCUUFBQUFnQUFBQUJBQUFBQUVBQUFBb0FBQUFDd0FBQUhOamFHVnRZVjl3WVhSb0FHei8vLzhRQUJRQUNBQUdBQWNBREFBQUFCQUFFQUFBQUFBQUFRVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUJBQUFBR2wwWlcwQUFBQUFwUC8vLzlqLy8vOEFBQUFGRUFBQUFCZ0FBQUFFQUFBQUFBQUFBQWNBQUFCd2NtOW1hV3hsQU16Ly8vOFFBQlFBQ0FBQUFBY0FEQUFBQUJBQUVBQUFBQUFBQUFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQXdBQUFITnhiQUFFQUFRQUJBQUFBUC8vLy8rWUFnQUFGQUFBQUFBQUFBQU1BQllBQmdBRkFBZ0FEQUFNQUFBQUFBTUVBQmdBQUFDZ0FBQUFBQUFBQUFBQUNnQVlBQXdBQkFBSUFBb0FBQUM4QVFBQUVBQUFBQUVBQUFBQUFBQUFBQUFBQUJvQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBSUFBQUFBQUFBQUFnQUFBQUFBQUFBQlFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUlBQUFBQUFBQUFCZ0FBQUFBQUFBQUh3QUFBQUFBQUFBNEFBQUFBQUFBQUFBQUFBQUFBQUFBT0FBQUFBQUFBQUFJQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBZ0FBQUFBQUFBQVNBQUFBQUFBQUFBRUFBQUFBQUFBQUZBQUFBQUFBQUFBQUFBQUFBQUFBQUJRQUFBQUFBQUFBQUFBQUFBQUFBQUFVQUFBQUFBQUFBQUlBQUFBQUFBQUFGZ0FBQUFBQUFBQUF3QUFBQUFBQUFCZ0FBQUFBQUFBQUFBQUFBQUFBQUFBWUFBQUFBQUFBQUFJQUFBQUFBQUFBR2dBQUFBQUFBQUFCZ0FBQUFBQUFBQndBQUFBQUFBQUFBQUFBQUFBQUFBQWNBQUFBQUFBQUFBSUFBQUFBQUFBQUhnQUFBQUFBQUFBQXdBQUFBQUFBQUNBQUFBQUFBQUFBQUFBQUFBQUFBQUFnQUFBQUFBQUFBQUlBQUFBQUFBQUFJZ0FBQUFBQUFBQUNnQUFBQUFBQUFDWUFBQUFBQUFBQUFBQUFBQUFBQUFBbUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBS0FBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFGQUFBQWJpQXFJRElBQUFBQUFBQUFId0FBQUhabmFTNWxlSEJ5WlhOemFXOXVMbVIxWTJ0a1lpNXpZMkZzWVhJdWRqRUFBQUFBQUFFQUFBQUFBQUFBQkFBQUFHMWhhVzRBQUFBQUFBQUFBQU1BQUFCVlZFTUFBQUFBQUFBQUFBQUdBQUFBWW1sdVlYSjVBQUFBQUFBQUF3QUFBR0Z6WXdBQUFBQUFBQUFBQUFvQUFBQnVkV3hzYzE5c1lYTjBBQUFBQUFBQUFRQUFBQUFBQUFELy8vLy9BQUFBQUE9PQAAAAAXAAAAdmdpLmNvbHVtbi5nZW5lcmF0ZWQudjIAeP///xgAAAAEAAAACAAAAHJldGFpbmVkAAAAAA0AAABwcm92aWRlci5ub3RlAAAAqP///xAAAAAEAAAAAAAAAAAAAAAHAAAAY29tbWVudADI////FAAAAAQAAAAHAAAAdmlydHVhbAAYAAAAdmdpLmNvbHVtbi5nZW5lcmF0aW9uLnYxAAAAAAgADAAEAAgACAAAAIgHAAAEAAAAeAcAAC8vLy8vMGdDQUFBUUFBQUFBQUFLQUF3QUJnQUZBQWdBQ2dBQUFBQUJCQUFNQUFBQUNBQUlBQUFBQkFBSUFBQUFCQUFBQUFRQUFBRHdBUUFBdEFFQUFFZ0JBQUFFQUFBQU1QNy8vd0FBQUEwa0FBQUFPQUFBQUFRQUFBQUZBQUFBL0FBQUFNd0FBQUNZQUFBQVlBQUFBQ0FBQUFBU0FBQUFaWFpoYkhWaGRHbHZibDlqYjI1MFpYaDBBQUJFL3YvL2VQNy8vd0FBQUFZUUFBQUFMQUFBQUFRQUFBQUFBQUFBR0FBQUFIQnlaWE5sY25abFgybHVjMlZ5ZEdsdmJsOXZjbVJsY2dBQUFBQ0Evdi8vdFA3Ly93QUFBQVVRQUFBQUpBQUFBQVFBQUFBQUFBQUFFZ0FBQUdSbFptRjFiSFJmYm5Wc2JGOXZjbVJsY2dBQXRQNy8vK2orLy84QUFBQUZFQUFBQUNBQUFBQUVBQUFBQUFBQUFBMEFBQUJrWldaaGRXeDBYMjl5WkdWeUFBQUE1UDcvL3hqLy8vOEFBQUFGRUFBQUFCd0FBQUFFQUFBQUFBQUFBQWtBQUFCamIyeHNZWFJwYjI0QUFBQVEvLy8vUlAvLy93QUFBQVVRQUFBQUhBQUFBQVFBQUFBQUFBQUFDQUFBQUhScGJXVjZiMjVsQUFBQUFEei8vLzl3Ly8vL0FBQUFEQlFBQUFBZ0FBQUFCQUFBQUFFQUFBQW9BQUFBQ3dBQUFITmphR1Z0WVY5d1lYUm9BR3ovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFCQUFBQUdsMFpXMEFBQUFBcFAvLy85ai8vLzhBQUFBRkVBQUFBQmdBQUFBRUFBQUFBQUFBQUFjQUFBQndjbTltYVd4bEFNei8vLzhRQUJRQUNBQUFBQWNBREFBQUFCQUFFQUFBQUFBQUFBVVFBQUFBR0FBQUFBUUFBQUFBQUFBQUF3QUFBSE54YkFBRUFBUUFCQUFBQVAvLy8vK1lBZ0FBRkFBQUFBQUFBQUFNQUJZQUJnQUZBQWdBREFBTUFBQUFBQU1FQUJnQUFBQ2dBQUFBQUFBQUFBQUFDZ0FZQUF3QUJBQUlBQW9BQUFDOEFRQUFFQUFBQUFFQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBZ0FBQUFBQUFBQUJRQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFJQUFBQUFBQUFBQmdBQUFBQUFBQUFId0FBQUFBQUFBQTRBQUFBQUFBQUFBQUFBQUFBQUFBQU9BQUFBQUFBQUFBSUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQWdBQUFBQUFBQUFTQUFBQUFBQUFBQUVBQUFBQUFBQUFGQUFBQUFBQUFBQUFBQUFBQUFBQUFCUUFBQUFBQUFBQUFBQUFBQUFBQUFBVUFBQUFBQUFBQUFJQUFBQUFBQUFBRmdBQUFBQUFBQUFBd0FBQUFBQUFBQmdBQUFBQUFBQUFBQUFBQUFBQUFBQVlBQUFBQUFBQUFBSUFBQUFBQUFBQUdnQUFBQUFBQUFBQmdBQUFBQUFBQUJ3QUFBQUFBQUFBQUFBQUFBQUFBQUFjQUFBQUFBQUFBQUlBQUFBQUFBQUFIZ0FBQUFBQUFBQUF3QUFBQUFBQUFDQUFBQUFBQUFBQUFBQUFBQUFBQUFBZ0FBQUFBQUFBQUFJQUFBQUFBQUFBSWdBQUFBQUFBQUFDZ0FBQUFBQUFBQ1lBQUFBQUFBQUFBQUFBQUFBQUFBQW1BQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUtBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBRkFBQUFiaUFxSURJQUFBQUFBQUFBSHdBQUFIWm5hUzVsZUhCeVpYTnphVzl1TG1SMVkydGtZaTV6WTJGc1lYSXVkakVBQUFBQUFBRUFBQUFBQUFBQUJBQUFBRzFoYVc0QUFBQUFBQUFBQUFNQUFBQlZWRU1BQUFBQUFBQUFBQUFHQUFBQVltbHVZWEo1QUFBQUFBQUFBd0FBQUdGell3QUFBQUFBQUFBQUFBb0FBQUJ1ZFd4c2MxOXNZWE4wQUFBQUFBQUFBUUFBQUFBQUFBRC8vLy8vQUFBQUFBPT0AAAAAFwAAAHZnaS5jb2x1bW4uZ2VuZXJhdGVkLnYxAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAA", + "invalid_base64": "/////6gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAABAAQAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAA/AAAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAAAYAAAABAAAAAsAAABub3RfYmFzZTY0IQAXAAAAdmdpLmNvbHVtbi5nZW5lcmF0ZWQudjEAxP///wAAAAFAAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAABwAAAAEAAAAAAAAAAEAAABuAAAACAAMAAgABwAIAAAAAAAAAUAAAAAAAAAA", + "base64_whitespace": "/////xgJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACwCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAbAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAACIBwAABAAAAHkHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNnQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFFQUFBQUFBQUFBRkFBQUFBQUFBQUFBQUFBQUFBQUFBQlFBQUFBQUFBQUFBQUFBQUFBQUFBQVVBQUFBQUFBQUFBSUFBQUFBQUFBQUZnQUFBQUFBQUFBQXdBQUFBQUFBQUJnQUFBQUFBQUFBQUFBQUFBQUFBQUFZQUFBQUFBQUFBQUlBQUFBQUFBQUFHZ0FBQUFBQUFBQUJnQUFBQUFBQUFCd0FBQUFBQUFBQUFBQUFBQUFBQUFBY0FBQUFBQUFBQUFJQUFBQUFBQUFBSGdBQUFBQUFBQUFBd0FBQUFBQUFBQ0FBQUFBQUFBQUFBQUFBQUFBQUFBQWdBQUFBQUFBQUFBSUFBQUFBQUFBQUlnQUFBQUFBQUFBQ2dBQUFBQUFBQUNZQUFBQUFBQUFBQUFBQUFBQUFBQUFtQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFCQUFBQUcxaGFXNEFBQUFBQUFBQUFBTUFBQUJWVkVNQUFBQUFBQUFBQUFBR0FBQUFZbWx1WVhKNUFBQUFBQUFBQXdBQUFHRnpZd0FBQUFBQUFBQUFBQW9BQUFCdWRXeHNjMTlzWVhOMEFBQUFBQUFBQVFBQUFBQUFBQUQvLy8vL0FBQUFBQT09CgAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQDE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAAAAAAA=", + "empty_payload": "/////6ABAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAAA4AQAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAA9AAAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAAAQAAAABAAAAAAAAAAAAAAAFwAAAHZnaS5jb2x1bW4uZ2VuZXJhdGVkLnYxAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAAAAAAAA==", + "invalid_ipc": "/////7gBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAABQAQAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAADAEAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAAAoAAAABAAAABgAAABibTkwSUdGdUlFbFFReUJ5WldOdmNtUT0AAAAAFwAAAHZnaS5jb2x1bW4uZ2VuZXJhdGVkLnYxAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAAAAAAAA==", + "invalid_utf8_sql": "/////xgJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACwCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAbAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAACIBwAABAAAAHgHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNnQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFFQUFBQUFBQUFBRkFBQUFBQUFBQUFBQUFBQUFBQUFBQlFBQUFBQUFBQUFBQUFBQUFBQUFBQVVBQUFBQUFBQUFBSUFBQUFBQUFBQUZnQUFBQUFBQUFBQXdBQUFBQUFBQUJnQUFBQUFBQUFBQUFBQUFBQUFBQUFZQUFBQUFBQUFBQUlBQUFBQUFBQUFHZ0FBQUFBQUFBQUJnQUFBQUFBQUFCd0FBQUFBQUFBQUFBQUFBQUFBQUFBY0FBQUFBQUFBQUFJQUFBQUFBQUFBSGdBQUFBQUFBQUFBd0FBQUFBQUFBQ0FBQUFBQUFBQUFBQUFBQUFBQUFBQWdBQUFBQUFBQUFBSUFBQUFBQUFBQUlnQUFBQUFBQUFBQ2dBQUFBQUFBQUNZQUFBQUFBQUFBQUFBQUFBQUFBQUFtQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlEL0lESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFCQUFBQUcxaGFXNEFBQUFBQUFBQUFBTUFBQUJWVkVNQUFBQUFBQUFBQUFBR0FBQUFZbWx1WVhKNUFBQUFBQUFBQXdBQUFHRnpZd0FBQUFBQUFBQUFBQW9BQUFCdWRXeHNjMTlzWVhOMEFBQUFBQUFBQVFBQUFBQUFBQUQvLy8vL0FBQUFBQT09AAAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQDE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAAAAAAA=", + "trailing_ipc": "/////yAJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAAC4CAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAdAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAACQBwAABAAAAIAHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNnQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFFQUFBQUFBQUFBRkFBQUFBQUFBQUFBQUFBQUFBQUFBQlFBQUFBQUFBQUFBQUFBQUFBQUFBQVVBQUFBQUFBQUFBSUFBQUFBQUFBQUZnQUFBQUFBQUFBQXdBQUFBQUFBQUJnQUFBQUFBQUFBQUFBQUFBQUFBQUFZQUFBQUFBQUFBQUlBQUFBQUFBQUFHZ0FBQUFBQUFBQUJnQUFBQUFBQUFCd0FBQUFBQUFBQUFBQUFBQUFBQUFBY0FBQUFBQUFBQUFJQUFBQUFBQUFBSGdBQUFBQUFBQUFBd0FBQUFBQUFBQ0FBQUFBQUFBQUFBQUFBQUFBQUFBQWdBQUFBQUFBQUFBSUFBQUFBQUFBQUlnQUFBQUFBQUFBQ2dBQUFBQUFBQUNZQUFBQUFBQUFBQUFBQUFBQUFBQUFtQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFCQUFBQUcxaGFXNEFBQUFBQUFBQUFBTUFBQUJWVkVNQUFBQUFBQUFBQUFBR0FBQUFZbWx1WVhKNUFBQUFBQUFBQXdBQUFHRnpZd0FBQUFBQUFBQUFBQW9BQUFCdWRXeHNjMTlzWVhOMEFBQUFBQUFBQVFBQUFBQUFBQUQvLy8vL0FBQUFBSFJ5WVdsc2FXNW4AAAAAFwAAAHZnaS5jb2x1bW4uZ2VuZXJhdGVkLnYxAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAAAAAAAA==", + "wrong_record": "//////gFAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACQBQAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAATAUAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAABoBAAABAAAAFgEAAAvLy8vLzFBQkFBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBVUFBQUR3QUFBQXNBQUFBSHdBQUFCRUFBQUFCQUFBQURULy8vOEFBQUFHRUFBQUFDd0FBQUFFQUFBQUFBQUFBQmdBQUFCd2NtVnpaWEoyWlY5cGJuTmxjblJwYjI1ZmIzSmtaWElBQUFBQU5QLy8vM0QvLy84QUFBQUZFQUFBQUNRQUFBQUVBQUFBQUFBQUFCSUFBQUJrWldaaGRXeDBYMjUxYkd4ZmIzSmtaWElBQUdqLy8vK2svLy8vQUFBQUJSQUFBQUFnQUFBQUJBQUFBQUFBQUFBTkFBQUFaR1ZtWVhWc2RGOXZjbVJsY2dBQUFKai8vLy9VLy8vL0FBQUFCUkFBQUFBY0FBQUFCQUFBQUFBQUFBQUpBQUFBWTI5c2JHRjBhVzl1QUFBQXhQLy8veEFBRkFBSUFBQUFCd0FNQUFBQUVBQVFBQUFBQUFBQUJSQUFBQUFnQUFBQUJBQUFBQUFBQUFBSUFBQUFkR2x0WlhwdmJtVUFBQUFBQkFBRUFBUUFBQUQvLy8vL2lBRUFBQlFBQUFBQUFBQUFEQUFXQUFZQUJRQUlBQXdBREFBQUFBQURCQUFZQUFBQVVBQUFBQUFBQUFBQUFBb0FHQUFNQUFRQUNBQUtBQUFBL0FBQUFCQUFBQUFCQUFBQUFBQUFBQUFBQUFBT0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUNBQUFBQUFBQUFBSUFBQUFBQUFBQUFNQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFDQUFBQUFBQUFBQVlBQUFBQUFBQUFBWUFBQUFBQUFBQUlBQUFBQUFBQUFBQUFBQUFBQUFBQUNBQUFBQUFBQUFBQ0FBQUFBQUFBQUFvQUFBQUFBQUFBQU1BQUFBQUFBQUFNQUFBQUFBQUFBQUFBQUFBQUFBQUFEQUFBQUFBQUFBQUNBQUFBQUFBQUFBNEFBQUFBQUFBQUFvQUFBQUFBQUFBU0FBQUFBQUFBQUFBQUFBQUFBQUFBRWdBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQlFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU1BQUFCVlZFTUFBQUFBQUFBQUFBQUdBQUFBWW1sdVlYSjVBQUFBQUFBQUF3QUFBR0Z6WXdBQUFBQUFBQUFBQUFvQUFBQnVkV3hzYzE5c1lYTjBBQUFBQUFBQUFRQUFBQUFBQUFELy8vLy9BQUFBQUE9PQAAAAAXAAAAdmdpLmNvbHVtbi5nZW5lcmF0ZWQudjEAxP///wAAAAFAAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAABwAAAAEAAAAAAAAAAEAAABuAAAACAAMAAgABwAIAAAAAAAAAUAAAAAAAAAA", + "missing_context": "/////+gIAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACECAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAQAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAABcBwAABAAAAEwHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNBQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFFQUFBQUFBQUFBRkFBQUFBQUFBQUFBUUFBQUFBQUFBQllBQUFBQUFBQUFBQUFBQUFBQUFBQVdBQUFBQUFBQUFBSUFBQUFBQUFBQUdBQUFBQUFBQUFBQUFBQUFBQUFBQUJnQUFBQUFBQUFBQUFBQUFBQUFBQUFZQUFBQUFBQUFBQUlBQUFBQUFBQUFHZ0FBQUFBQUFBQUFBQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBYUFBQUFBQUFBQUFJQUFBQUFBQUFBSEFBQUFBQUFBQUFBQUFBQUFBQUFBQndBQUFBQUFBQUFBQUFBQUFBQUFBQWNBQUFBQUFBQUFBSUFBQUFBQUFBQUhnQUFBQUFBQUFBQUFBQUFBQUFBQUI0QUFBQUFBQUFBQUFBQUFBQUFBQUFlQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFRQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFCQUFBQUcxaGFXNEFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQS8vLy8vd0FBQUFBPQAAAAAXAAAAdmdpLmNvbHVtbi5nZW5lcmF0ZWQudjEAxP///wAAAAFAAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAABwAAAAEAAAAAAAAAAEAAABuAAAACAAMAAgABwAIAAAAAAAAAUAAAAA=", + "invalid_context": "/////xgJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACwCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAbAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAACIBwAABAAAAHgHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNnQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFFQUFBQUFBQUFBRkFBQUFBQUFBQUFBQUFBQUFBQUFBQlFBQUFBQUFBQUFBQUFBQUFBQUFBQVVBQUFBQUFBQUFBSUFBQUFBQUFBQUZnQUFBQUFBQUFBQXdBQUFBQUFBQUJnQUFBQUFBQUFBQUFBQUFBQUFBQUFZQUFBQUFBQUFBQUlBQUFBQUFBQUFHZ0FBQUFBQUFBQUJnQUFBQUFBQUFCd0FBQUFBQUFBQUFBQUFBQUFBQUFBY0FBQUFBQUFBQUFJQUFBQUFBQUFBSGdBQUFBQUFBQUFDQUFBQUFBQUFBQ0FBQUFBQUFBQUFBQUFBQUFBQUFBQWdBQUFBQUFBQUFBSUFBQUFBQUFBQUlnQUFBQUFBQUFBQ2dBQUFBQUFBQUNZQUFBQUFBQUFBQUFBQUFBQUFBQUFtQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFCQUFBQUcxaGFXNEFBQUFBQUFBQUFBTUFBQUJWVkVNQUFBQUFBQUFBQUFBR0FBQUFZbWx1WVhKNUFBQUFBQUFBQ0FBQUFITnBaR1YzWVhsekFBQUFBQW9BQUFCdWRXeHNjMTlzWVhOMEFBQUFBQUFBQVFBQUFBQUFBQUQvLy8vL0FBQUFBQT09AAAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQDE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAAAAAAA=", + "empty_sql": "/////wgJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACkCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAYAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAAB8BwAABAAAAGwHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNZQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFBQUFBQUFBQUFBQUlBQUFBQUFBQUFBQUFBQUFBQUFBQUNBQUFBQUFBQUFBSUFBQUFBQUFBQUJBQUFBQUFBQUFBSHdBQUFBQUFBQUF3QUFBQUFBQUFBQUFBQUFBQUFBQUFNQUFBQUFBQUFBQUlBQUFBQUFBQUFEZ0FBQUFBQUFBQUFBQUFBQUFBQUFBNEFBQUFBQUFBQUFnQUFBQUFBQUFBUUFBQUFBQUFBQUFFQUFBQUFBQUFBRWdBQUFBQUFBQUFBQUFBQUFBQUFBQklBQUFBQUFBQUFBQUFBQUFBQUFBQVNBQUFBQUFBQUFBSUFBQUFBQUFBQUZBQUFBQUFBQUFBQXdBQUFBQUFBQUJZQUFBQUFBQUFBQUFBQUFBQUFBQUFXQUFBQUFBQUFBQUlBQUFBQUFBQUFHQUFBQUFBQUFBQUJnQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBYUFBQUFBQUFBQUFJQUFBQUFBQUFBSEFBQUFBQUFBQUFBd0FBQUFBQUFBQjRBQUFBQUFBQUFBQUFBQUFBQUFBQWVBQUFBQUFBQUFBSUFBQUFBQUFBQUlBQUFBQUFBQUFBQ2dBQUFBQUFBQUNRQUFBQUFBQUFBQUFBQUFBQUFBQUFrQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCOEFBQUIyWjJrdVpYaHdjbVZ6YzJsdmJpNWtkV05yWkdJdWMyTmhiR0Z5TG5ZeEFBQUFBQUFCQUFBQUFBQUFBQVFBQUFCdFlXbHVBQUFBQUFBQUFBQURBQUFBVlZSREFBQUFBQUFBQUFBQUJnQUFBR0pwYm1GeWVRQUFBQUFBQUFNQUFBQmhjMk1BQUFBQUFBQUFBQUFLQUFBQWJuVnNiSE5mYkdGemRBQUFBQUFBQUFFQUFBQUFBQUFBLy8vLy93QUFBQUE9AAAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQDE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAA==", + "owner_mismatch": "/////yAJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAAC4CAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAdAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAACQBwAABAAAAIAHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNvQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBSHdBQUFBQUFBQUE0QUFBQUFBQUFBQUFBQUFBQUFBQUFPQUFBQUFBQUFBQUlBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFnQUFBQUFBQUFBU0FBQUFBQUFBQUFKQUFBQUFBQUFBRmdBQUFBQUFBQUFBQUFBQUFBQUFBQllBQUFBQUFBQUFBQUFBQUFBQUFBQVdBQUFBQUFBQUFBSUFBQUFBQUFBQUdBQUFBQUFBQUFBQXdBQUFBQUFBQUJvQUFBQUFBQUFBQUFBQUFBQUFBQUFhQUFBQUFBQUFBQUlBQUFBQUFBQUFIQUFBQUFBQUFBQUJnQUFBQUFBQUFCNEFBQUFBQUFBQUFBQUFBQUFBQUFBZUFBQUFBQUFBQUFJQUFBQUFBQUFBSUFBQUFBQUFBQUFBd0FBQUFBQUFBQ0lBQUFBQUFBQUFBQUFBQUFBQUFBQWlBQUFBQUFBQUFBSUFBQUFBQUFBQUpBQUFBQUFBQUFBQ2dBQUFBQUFBQUNnQUFBQUFBQUFBQUFBQUFBQUFBQUFvQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUh3QUFBSFpuYVM1bGVIQnlaWE56YVc5dUxtUjFZMnRrWWk1elkyRnNZWEl1ZGpFQUFBQUFBQUVBQUFBQUFBQUFDUUFBQUdWc2MyVjNhR1Z5WlFBQUFBQUFBQUFBQUFBQUF3QUFBRlZVUXdBQUFBQUFBQUFBQUFZQUFBQmlhVzVoY25rQUFBQUFBQUFEQUFBQVlYTmpBQUFBQUFBQUFBQUFDZ0FBQUc1MWJHeHpYMnhoYzNRQUFBQUFBQUFCQUFBQUFBQUFBUC8vLy84QUFBQUEAAAAAFwAAAHZnaS5jb2x1bW4uZ2VuZXJhdGVkLnYxAMT///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAcAAAABAAAAAAAAAABAAAAbgAAAAgADAAIAAcACAAAAAAAAAFAAAAAAAAAAA==", + "unsupported_profile": "/////wgJAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAIAAACkCAAAGAAAAAAAEgAYAAgABgAHAAwAAAAQABQAEgAAAAAAAQIUAAAAYAgAAAgAAAAYAAAAAAAAAAgAAABjb21wdXRlZAAAAAAEAAAAoAAAAFwAAAA4AAAABAAAAHj///8YAAAABAAAAAgAAAByZXRhaW5lZAAAAAANAAAAcHJvdmlkZXIubm90ZQAAAKj///8QAAAABAAAAAAAAAAAAAAABwAAAGNvbW1lbnQAyP///xQAAAAEAAAABwAAAHZpcnR1YWwAGAAAAHZnaS5jb2x1bW4uZ2VuZXJhdGlvbi52MQAAAAAIAAwABAAIAAgAAAB8BwAABAAAAGwHAAAvLy8vLzBnQ0FBQVFBQUFBQUFBS0FBd0FCZ0FGQUFnQUNnQUFBQUFCQkFBTUFBQUFDQUFJQUFBQUJBQUlBQUFBQkFBQUFBUUFBQUR3QVFBQXRBRUFBRWdCQUFBRUFBQUFNUDcvL3dBQUFBMGtBQUFBT0FBQUFBUUFBQUFGQUFBQS9BQUFBTXdBQUFDWUFBQUFZQUFBQUNBQUFBQVNBQUFBWlhaaGJIVmhkR2x2Ymw5amIyNTBaWGgwQUFCRS92Ly9lUDcvL3dBQUFBWVFBQUFBTEFBQUFBUUFBQUFBQUFBQUdBQUFBSEJ5WlhObGNuWmxYMmx1YzJWeWRHbHZibDl2Y21SbGNnQUFBQUNBL3YvL3RQNy8vd0FBQUFVUUFBQUFKQUFBQUFRQUFBQUFBQUFBRWdBQUFHUmxabUYxYkhSZmJuVnNiRjl2Y21SbGNnQUF0UDcvLytqKy8vOEFBQUFGRUFBQUFDQUFBQUFFQUFBQUFBQUFBQTBBQUFCa1pXWmhkV3gwWDI5eVpHVnlBQUFBNVA3Ly94ai8vLzhBQUFBRkVBQUFBQndBQUFBRUFBQUFBQUFBQUFrQUFBQmpiMnhzWVhScGIyNEFBQUFRLy8vL1JQLy8vd0FBQUFVUUFBQUFIQUFBQUFRQUFBQUFBQUFBQ0FBQUFIUnBiV1Y2YjI1bEFBQUFBRHovLy85dy8vLy9BQUFBREJRQUFBQWdBQUFBQkFBQUFBRUFBQUFvQUFBQUN3QUFBSE5qYUdWdFlWOXdZWFJvQUd6Ly8vOFFBQlFBQ0FBR0FBY0FEQUFBQUJBQUVBQUFBQUFBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQkFBQUFHbDBaVzBBQUFBQXBQLy8vOWovLy84QUFBQUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBY0FBQUJ3Y205bWFXeGxBTXovLy84UUFCUUFDQUFBQUFjQURBQUFBQkFBRUFBQUFBQUFBQVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBd0FBQUhOeGJBQUVBQVFBQkFBQUFQLy8vLytZQWdBQUZBQUFBQUFBQUFBTUFCWUFCZ0FGQUFnQURBQU1BQUFBQUFNRUFCZ0FBQUNZQUFBQUFBQUFBQUFBQ2dBWUFBd0FCQUFJQUFvQUFBQzhBUUFBRUFBQUFBRUFBQUFBQUFBQUFBQUFBQm9BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFJQUFBQUFBQUFBQWdBQUFBQUFBQUFCUUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBSUFBQUFBQUFBQUJnQUFBQUFBQUFBRXdBQUFBQUFBQUF3QUFBQUFBQUFBQUFBQUFBQUFBQUFNQUFBQUFBQUFBQUlBQUFBQUFBQUFEZ0FBQUFBQUFBQUFBQUFBQUFBQUFBNEFBQUFBQUFBQUFnQUFBQUFBQUFBUUFBQUFBQUFBQUFFQUFBQUFBQUFBRWdBQUFBQUFBQUFBQUFBQUFBQUFBQklBQUFBQUFBQUFBQUFBQUFBQUFBQVNBQUFBQUFBQUFBSUFBQUFBQUFBQUZBQUFBQUFBQUFBQXdBQUFBQUFBQUJZQUFBQUFBQUFBQUFBQUFBQUFBQUFXQUFBQUFBQUFBQUlBQUFBQUFBQUFHQUFBQUFBQUFBQUJnQUFBQUFBQUFCb0FBQUFBQUFBQUFBQUFBQUFBQUFBYUFBQUFBQUFBQUFJQUFBQUFBQUFBSEFBQUFBQUFBQUFBd0FBQUFBQUFBQjRBQUFBQUFBQUFBQUFBQUFBQUFBQWVBQUFBQUFBQUFBSUFBQUFBQUFBQUlBQUFBQUFBQUFBQ2dBQUFBQUFBQUNRQUFBQUFBQUFBQUFBQUFBQUFBQUFrQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFLQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQUFBQUFBQUFBQUFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQkFBQUFBQUFBQUFBQUFBQUFBQUFBQVFBQUFBQUFBQUFBQUFBQUFBQUFBQUVBQUFBQUFBQUFBQUFBQUFBQUFBQUJBQUFBQUFBQUFBQUFBQUFBQUFBQUFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZBQUFBYmlBcUlESUFBQUFBQUFBQUV3QUFBRzkwYUdWeUxtVjRjSEpsYzNOcGIyNHVkakVBQUFBQUFBQUFBQUFCQUFBQUFBQUFBQVFBQUFCdFlXbHVBQUFBQUFBQUFBQURBQUFBVlZSREFBQUFBQUFBQUFBQUJnQUFBR0pwYm1GeWVRQUFBQUFBQUFNQUFBQmhjMk1BQUFBQUFBQUFBQUFLQUFBQWJuVnNiSE5mYkdGemRBQUFBQUFBQUFFQUFBQUFBQUFBLy8vLy93QUFBQUE9AAAAABcAAAB2Z2kuY29sdW1uLmdlbmVyYXRlZC52MQDE////AAAAAUAAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQIQAAAAHAAAAAQAAAAAAAAAAQAAAG4AAAAIAAwACAAHAAgAAAAAAAABQAAAAA==" + } + } +} diff --git a/conformance/projection-expressions/README.md b/conformance/projection-expressions/README.md new file mode 100644 index 00000000..60053d9b --- /dev/null +++ b/conformance/projection-expressions/README.md @@ -0,0 +1,33 @@ +# Scalar projection worker proof + +Run `python conformance/projection-expressions/run_worker.py --worker COMMAND` +from the Python checkout. An HTTP URL is accepted as well. The runner uses the +typed public bind, proposal and init RPCs against the common example fixture; +it does not simulate optimizer replacement. + +The fixture is `example.main.projection_rows`, backed by +`projection_sequence()` with no arguments. It emits nullable `n: int32` with +values 0 through 9 and declares `STABLE_WITHIN_QUERY`. It offers +`vgi.expression.duckdb.scalar.v1` and accepts +exactly `CAST("n" AS BIGINT)` under the default evaluation context. Requested +result fields, including their metadata, append unchanged. Other expressions +or contexts decline. The Python reference is +`env VGI_PROJECTION_COMMON=1 python -m tests.projection_reference`. + +The runner verifies ordinary scan, partial acceptance, complete decline, +unsupported-context decline, complete acceptance, reuse of accepted bind +state, and an independent ordinary rebind. It requires exact result schemas +and rows throughout, including preservation of repeatability. Thirteen malformed wire requests also verify rejection of +bad expression IPC, counts, names, metadata, owners, offers, contexts and outer +row/schema shape and null/unknown repeatability. The runner has 19 cases in total. +Native optimizer replacement and malformed-provider +responses have separate tests; this runner does not claim those properties. + +Add `--wrapped-framing` to qualify raw outer IPC handling: valid physical EOF +and legacy EOS, extra batches, trailing bytes, repeated EOS, truncated EOS/body +and requests exceeding 8 MiB. This adds eight cases, for 27 total. The paired +Go RPC dependency passes this mode over subprocess and HTTP; its unit tests +also check rejection before Arrow decoding. Python passes the 19 semantic +cases but its generic RPC wrapper accepts an extra outer batch before typed +validation; complete Python outer framing/raw-size rejection remains an open +release requirement. See the manifest checkpoint for each SDK's evidence. diff --git a/conformance/projection-expressions/run_worker.py b/conformance/projection-expressions/run_worker.py new file mode 100644 index 00000000..2140dda6 --- /dev/null +++ b/conformance/projection-expressions/run_worker.py @@ -0,0 +1,294 @@ +# Copyright 2026 Query Farm LLC - https://query.farm + +"""Exercise optional projection negotiation and execution through public RPCs. + +The portable fixture is example/main/projection_rows, backed by +projection_sequence(). It emits nullable int32 n=0..9 and accepts only the +canonical CAST("n" AS BIGINT) expression in the default evaluation context. +""" + +from __future__ import annotations + +import argparse +import json +from contextlib import closing +from dataclasses import replace + +import pyarrow as pa +from vgi_rpc.rpc import RpcError + +from vgi.arguments import Arguments +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import SqlExpression +from vgi.client import Client +from vgi.invocation import BindRepeatability, BindResponse, FunctionType +from vgi.protocol import ( + BindRequest, + ClientCapabilities, + InitRequest, + TableFunctionProjectionRequest, +) + + +def proposal(bind_call, bind_result, sql, *, context=None): + """Construct typed, canonical candidate fields in stable proposal order.""" + return TableFunctionProjectionRequest( + bind_call=bind_call, + bind_result=bind_result, + expressions=[ + SqlExpression( + sql=text, + profile=ROW_SCALAR_EXPRESSION, + schema_path=["main"], + evaluation_context=context or QueryEvaluationContext(), + ).serialize_to_bytes() + for text in sql + ], + result_schema=pa.schema( + [ + pa.field(f"wide_{index}", pa.int64(), metadata={b"projection-fixture": str(index).encode()}) + for index in range(len(sql)) + ] + ), + ) + + +def scan(proxy, bind_call, bind_result): + """Consume the existing producer stream with the selected bind contract.""" + with closing( + proxy.init( + request=InitRequest( + bind_call=bind_call, + output_schema=bind_result.output_schema, + bind_opaque_data=bind_result.opaque_data, + ) + ) + ) as stream: + batches = [output.batch for output in stream] + for batch in batches: + assert batch.schema.equals(bind_result.output_schema, check_metadata=True), batch.schema + actual = pa.Table.from_batches(batches, schema=bind_result.output_schema) + assert actual.to_pylist() == [ + {field.name: number for field in bind_result.output_schema} for number in range(10) + ], actual.to_pylist() + + +class RawProjectionRequest(TableFunctionProjectionRequest): + """Send deliberately malformed wire bytes without local model validation.""" + + def serialize_to_bytes(self): + """Return the intentional invalid test payload unchanged.""" + return self.raw_payload + + +def raw_request(request, field, value): + """Replace one packed field while retaining the exact outer wire schema.""" + with pa.ipc.open_stream(request.serialize_to_bytes()) as reader: + batch = reader.read_next_batch() + index = batch.schema.get_field_index(field) + columns = list(batch.columns) + columns[index] = pa.array([value], type=batch.schema.field(index).type) + return raw_batch(pa.RecordBatch.from_arrays(columns, schema=batch.schema)) + + +def raw_batch(batch): + """Pack an intentionally invalid row count or schema for wire tests.""" + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, batch.schema) as writer: + writer.write_batch(batch) + return raw_payload(sink.getvalue().to_pybytes()) + + +def raw_payload(payload): + """Wrap exact bytes without normalizing their IPC framing.""" + invalid = object.__new__(RawProjectionRequest) + object.__setattr__(invalid, "raw_payload", payload) + return invalid + + +def wrapped_framing_cases(request): + """Return valid EOF alternatives and malformed single-record envelopes.""" + encoded = request.serialize_to_bytes() + assert encoded.endswith(b"\xff\xff\xff\xff\x00\x00\x00\x00") + with pa.ipc.open_stream(encoded) as reader: + batch = reader.read_next_batch() + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, batch.schema) as writer: + writer.write_batch(batch) + writer.write_batch(batch) + return [ + ("physical EOF", encoded[:-8], True), + ("legacy EOS", encoded[:-8] + b"\x00\x00\x00\x00", True), + ("extra IPC batch", sink.getvalue().to_pybytes(), False), + ("trailing IPC bytes", encoded + b"garbage", False), + ("repeated EOS", encoded + encoded[-8:], False), + ("truncated EOS", encoded[:-1], False), + ("truncated record body", encoded[:-9], False), + ("oversized wrapped request", encoded + bytes(8 * 1024 * 1024), False), + ] + + +def invalid_cases(request): + """Exercise handler boundaries independently of Python's local validators.""" + expressions = request.expression_records() + return [ + ("empty expressions", "expressions", []), + ( + "unknown repeatability", + "bind_result", + raw_request(request.bind_result, "repeatability", "ALWAYS").serialize_to_bytes(), + ), + ( + "null repeatability", + "bind_result", + raw_request(request.bind_result, "repeatability", None).serialize_to_bytes(), + ), + ("malformed expression IPC", "expressions", [b"invalid", request.expressions[1]]), + ("wrong result count", "result_schema", pa.schema([]).serialize().to_pybytes()), + ( + "source/result name collision", + "result_schema", + pa.schema([pa.field("N", pa.int64()), pa.field("wide_1", pa.int64())]).serialize().to_pybytes(), + ), + ( + "unexpected result schema metadata", + "result_schema", + request.result_schema.with_metadata({b"not": b"a new source"}).serialize().to_pybytes(), + ), + ("absent owner", "bind_call", replace(request.bind_call, schema_path=None).serialize_to_bytes()), + ( + "unoffered profile", + "bind_result", + replace(request.bind_result, projection_expression_profiles=[]).serialize_to_bytes(), + ), + ( + "different expression contexts", + "expressions", + [ + request.expressions[0], + replace( + expressions[1], evaluation_context=replace(QueryEvaluationContext(), timezone="Europe/Paris") + ).serialize_to_bytes(), + ], + ), + ] + + +def run(worker, *, wrapped_framing=False): + """Return named successes, failing immediately on any protocol mismatch.""" + client = Client.from_http(worker) if worker.startswith(("http://", "https://")) else Client(worker, pool=None) + results = [] + with client: + attached = client.catalog_attach( + name="example", + data_version_spec=None, + implementation_version=None, + client_capabilities=ClientCapabilities( + engine="projection-conformance", native_formats=[], catalogs=[], can_stream=True, filter_encodings=[] + ), + ) + client.table_get(attach_opaque_data=attached.attach_opaque_data, schema_path=["main"], name="projection_rows") + bind_call = BindRequest( + function_name="projection_sequence", + schema_path=["main"], + function_type=FunctionType.TABLE, + arguments=Arguments(), + attach_opaque_data=attached.attach_opaque_data, + ) + # The low-level typed proxy intentionally avoids Client.table_function(), + # which would perform a fresh ordinary bind and hide accepted state. + assert client._primary is not None + proxy = client._primary.proxy + original = proxy.bind(request=bind_call) + assert original.projection_expression_profiles == [ROW_SCALAR_EXPRESSION] + assert original.repeatability is BindRepeatability.STABLE_WITHIN_QUERY + assert original.output_schema.equals(pa.schema([pa.field("n", pa.int32())]), check_metadata=True) + scan(proxy, bind_call, original) + results.append("ordinary scan before negotiation") + + cases = [ + ("partial acceptance", ['CAST("n" AS BIGINT)', '"n" + 1', 'CAST("n" AS BIGINT)'], [0, 2], None), + ("complete decline", ['"n" + 1'], [], None), + ( + "unsupported context decline", + ['CAST("n" AS BIGINT)'], + [], + replace(QueryEvaluationContext(), timezone="Europe/Paris"), + ), + ("complete acceptance", ['CAST("n" AS BIGINT)'], [0], None), + ] + for name, expressions, expected_indices, context in cases: + request = proposal(bind_call, original, expressions, context=context) + response = proxy.table_function_projection(request=request) + response.validate_request(request) + assert response.accepted_indices == expected_indices, response + selected: BindResponse = response.bind_result or original + assert selected.repeatability is original.repeatability + scan(proxy, bind_call, selected) + # Reusing accepted bind bytes must retain the accepted schema/state. + if expected_indices: + scan(proxy, bind_call, selected) + results.append(name) + + valid = proposal(bind_call, original, ['CAST("n" AS BIGINT)', 'CAST("n" AS BIGINT)']) + if wrapped_framing: + for name, payload, accepted in wrapped_framing_cases(valid): + try: + response = proxy.table_function_projection(request=raw_payload(payload)) + except RpcError: + if accepted: + raise + else: + if not accepted: + raise AssertionError("malformed wrapped request was not rejected: " + name) + response.validate_request(valid) + assert response.accepted_indices == [0, 1] + results.append(("accept " if accepted else "reject ") + name) + for name, field, value in invalid_cases(valid): + try: + proxy.table_function_projection(request=raw_request(valid, field, value)) + except RpcError: + results.append("reject " + name) + else: + raise AssertionError("malformed proposal was not rejected: " + name) + + with pa.ipc.open_stream(valid.serialize_to_bytes()) as reader: + row = reader.read_next_batch() + malformed = [ + ("zero proposal rows", row.slice(0, 0)), + ( + "multiple proposal rows", + pa.RecordBatch.from_arrays([pa.concat_arrays([column, column]) for column in row], schema=row.schema), + ), + ("extra proposal field", row.append_column("unexpected", pa.array([1]))), + ] + for name, batch in malformed: + try: + proxy.table_function_projection(request=raw_batch(batch)) + except RpcError: + results.append("reject " + name) + else: + raise AssertionError("malformed proposal envelope was not rejected: " + name) + + rebound = proxy.bind(request=bind_call) + assert rebound.repeatability is original.repeatability + assert rebound.output_schema.equals(original.output_schema, check_metadata=True) + scan(proxy, bind_call, rebound) + results.append("independent ordinary rebind") + client.catalog_detach(attach_opaque_data=attached.attach_opaque_data) + return results + + +def main(): + """Run the same transport-level proof against any example worker.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--worker", required=True, help="Worker command or HTTP URL") + parser.add_argument("--wrapped-framing", action="store_true", help="Also qualify raw outer IPC framing and size") + args = parser.parse_args() + results = run(args.worker, wrapped_framing=args.wrapped_framing) + print(json.dumps({"passed": len(results), "cases": results})) + + +if __name__ == "__main__": + main() diff --git a/docs/api/arguments.md b/docs/api/arguments.md index 664ad9b0..d7a7f0cf 100644 --- a/docs/api/arguments.md +++ b/docs/api/arguments.md @@ -8,6 +8,10 @@ schemas from these declarations and validates inputs before your function runs. ::: vgi.arguments +## Typed values + +::: vgi.typed_values + ## Argument specifications ::: vgi.argument_spec diff --git a/docs/api/catalogs.md b/docs/api/catalogs.md index 6bb5c7c3..24cafa69 100644 --- a/docs/api/catalogs.md +++ b/docs/api/catalogs.md @@ -4,4 +4,44 @@ A worker can expose a database-like catalog — schemas, tables, views, macros, and settings — so DuckDB can `ATTACH` it. See the [Catalog Interface](../catalog-interface.md) guide for the conceptual overview. +Column comments and tags use the existing Arrow field metadata. The helpers +preserve missing versus empty values and leave unrelated metadata intact: + +```python +import pyarrow as pa +from vgi.catalog import ColumnMetadata, with_column_metadata + +amount = with_column_metadata( + pa.field("amount", pa.decimal128(12, 2)), + ColumnMetadata(comment="", tags={"unit": "USD"}), +) +``` + +Use the resulting field in the CREATE/ADD schema. `None` means the annotation is +absent; `""` and `{}` are present empty values. Readers can use +`column_metadata(field)` and `validate_column_metadata(schema)`. Malformed UTF-8, +invalid tag JSON, duplicate keys and non-string tag values reject before catalog +mutation. No additional capability flag is required. Native DuckDB 1.5 exposes +comments; tags remain in the catalog's Arrow schema until the engine adapter can +map them through a public column-tags API. + +Virtual generated columns can use the existing `Table` convenience: + +```python +from vgi.catalog import Table + +items = Table( + name="items", + columns=pa.schema([pa.field("n", pa.int64()), pa.field("doubled", pa.int64())]), + generated_columns={"doubled": "n * 2"}, +) +``` + +The helper emits canonical expression metadata with the containing schema as +owner. A scan supplies only the physical `n` column; DuckDB computes `doubled`. +The native 1.5 adapter supports bounded deterministic virtual expressions on +CREATE and discovery. Writes to generated tables, stored generation and ALTER +ADD of generated columns remain unsupported in this checkpoint. Custom catalog +providers must validate and persist definitions before accepting CREATE. + ::: vgi.catalog diff --git a/docs/api/client.md b/docs/api/client.md index 99310a1a..90319779 100644 --- a/docs/api/client.md +++ b/docs/api/client.md @@ -6,7 +6,7 @@ and surfaces errors as `ClientError`. It is the pure-Python counterpart to the D ## Discovering hosted protocols Every worker hosts the vgi-rpc reflection protocol, `vgi_rpc.Reflection.v1`, on every transport. -`Client.list_protocols()` asks it what the worker hosts, in the worker's order: `vgi.v2` first, +`Client.list_protocols()` asks it what the worker hosts, in the worker's order: `vgi.v3` first, then any protocols the worker adds through `Worker.hosted_protocols()`, then the framework's own. Each entry is a `HostedProtocol` (vgi-rpc's, re-exported from `vgi.client`) with `name`, `version`, `hash`, `deprecated`, `deprecation_message` and `features`. The hash is a SHA-256 hex digest of the @@ -27,7 +27,7 @@ with Client("vgi-fixture-worker") as client: A worker that does not host reflection (one built before it existed answers `UNIMPLEMENTED` / `protocol_not_supported`, which vgi-rpc raises as `ReflectionNotSupportedError`) is not an error for `list_protocols()`. It returns the single entry -`HostedProtocol("vgi.v2", "", "")`, because `vgi.v2` is the one protocol every worker hosts. The +`HostedProtocol("vgi.v3", "", "")`, because `vgi.v3` is the one protocol every worker hosts. The empty hash marks the entry as inferred rather than reported. Both calls use the client's primary connection. Do not call them while a stream is open on diff --git a/docs/api/worker.md b/docs/api/worker.md index 1f4b4ea1..4f690a55 100644 --- a/docs/api/worker.md +++ b/docs/api/worker.md @@ -57,10 +57,10 @@ The rules: - **The protocol is the unit of optionality.** There is no way to host part of a protocol. Make an optional capability its own protocol, and include it or leave it out. - **Names are routing keys.** Each protocol needs a distinct `protocol_name`. A repeated name, the - worker's own `vgi.v2`, or the reserved `vgi_rpc.` prefix stops the worker at startup, and the + worker's own `vgi.v3`, or the reserved `vgi_rpc.` prefix stops the worker at startup, and the error names `hosted_protocols()`. -- **`vgi.v2` is unaffected.** vgi-rpc routes each request on its `vgi_rpc.protocol` key and never - falls back to the primary protocol. A client that only speaks `vgi.v2`, such as the DuckDB +- **`vgi.v3` is unaffected.** vgi-rpc routes each request on its `vgi_rpc.protocol` key and never + falls back to the primary protocol. A client that only speaks `vgi.v3`, such as the DuckDB extension, sees exactly the behaviour it saw before. A `MetaWorker` (`vgi.meta_worker`) hosts the concatenation of its children's lists, in @@ -72,7 +72,7 @@ overriding the same hook stop it at startup. Reflection (`list_protocols`) reports protocols in this order: -| Transport | `vgi.v2` | `hosted_protocols()` | `vgi_rpc.Reflection.v1` | `vgi_rpc.Identity.v2` | +| Transport | `vgi.v3` | `hosted_protocols()` | `vgi_rpc.Reflection.v1` | `vgi_rpc.Identity.v2` | | --- | --- | --- | --- | --- | | stdin/stdout | yes | yes | yes | no | | AF_UNIX / named pipe | yes | yes | yes | no | diff --git a/docs/argument-serialization.md b/docs/argument-serialization.md index ddabd886..2db2f613 100644 --- a/docs/argument-serialization.md +++ b/docs/argument-serialization.md @@ -3,6 +3,56 @@ This document describes how VGI function argument specifications are serialized to Apache Arrow schemas for IPC transmission and DuckDB function registration. +## Bound argument values and metadata + +The specification below describes a function's signature. A call's bound values +use `Arguments`: one Arrow IPC record batch with one row and one `args` struct +column. Its children are named `positional_0`, `positional_1`, and so on, or +`named_`. This existing wire layout is unchanged. + +Use `Arguments.from_batch()` when values already have an Arrow schema. It accepts +a flat, one-row batch with those child names and retains field order, nullability, +recursive field metadata, and schema metadata. `to_batch()` returns that same +flat representation. Serialization wraps the fields in `args` and carries the +schema metadata on the IPC stream schema. Bind and init requests preserve it. + +```python +import pyarrow as pa +from vgi.arguments import Arguments + +schema = pa.schema([ + pa.field("positional_0", pa.int64(), metadata={b"example.unit": b"milliseconds"}), + pa.field("named_label", pa.string()), +], metadata={b"example.context": b"request"}) +batch = pa.RecordBatch.from_pylist([ + {"positional_0": 250, "named_label": None}, +], schema=schema) +arguments = Arguments.from_batch(batch) +restored = Arguments.deserialize_from_bytes(arguments.serialize_to_bytes()) +assert restored.schema().equals(schema, check_metadata=True) +assert restored.get("label", default="fallback") is None +``` + +Scalar callers can supply `argument_schema=` explicitly; otherwise the SDK +infers fields from the scalars as before. Supplied names and types must match +that schema. A typed NULL remains a present argument and does not select a +default. Missing arguments remain absent. `decode(StructScalar)` preserves +field metadata; a scalar cannot carry metadata from an enclosing stream schema. + +`vgi.typed_values.TypedValueBatch` provides the shared flat one-row carrier for +other typed-value contracts. It preserves the complete schema, checks declared +nullability recursively, and rejects duplicate names, wrong row counts, extra +record batches, and trailing bytes. `validate_schema()` checks exact schema +agreement, including metadata; it never casts values implicitly. A zero-column +batch still contains one row. `Arguments` additionally accepts DuckDB 1.5's +existing null empty `args` struct as an empty argument set. + +Metadata preservation does not establish support for the logical type it +describes. Consumers must validate required type annotations against their +capabilities. Remote-query parameter mapping, preparation/execution binding, +and DuckDB 2.0 logical-type semantics remain part of the combined protocol work; +this carrier does not advertise those features. + ## Quick Reference | Metadata Key | Value | Meaning | diff --git a/docs/catalog-interface.md b/docs/catalog-interface.md index eac56a18..8b87e696 100644 --- a/docs/catalog-interface.md +++ b/docs/catalog-interface.md @@ -78,6 +78,30 @@ Returned by `catalog_attach()` with attachment metadata: > a value minted for one principal cannot be replayed by another. Your implementation only ever sees the > plaintext; the sealing and unsealing happen transparently in the worker. +### Client capabilities at attach + +`Client.catalog_attach(..., client_capabilities=offer)` accepts the existing +`vgi.protocol.ClientCapabilities` record. The worker passes it to +`CatalogInterface.catalog_attach` as the keyword argument `client_capabilities`. +Custom overrides must accept this argument (usually +`client_capabilities: ClientCapabilities | None = None`) and forward it when +delegating to another catalog implementation. This is an SDK hook change; the +wire record and protocol version are unchanged. + +The offer describes the consuming engine's native formats, companion catalogs, +streaming support, and filter encodings. `None` means support is unspecified. +It does not imply support for optional behavior, and the offer is not an +authorization grant. Attach-ticket redemption keeps the redeeming client's +offer rather than recording the ticket creator's capabilities. + +If catalog behavior depends on the offer, retain the selected behavior in +attachment-scoped shared storage or in `attach_opaque_data`. Later requests may +reach another worker process. Do not put one client's agreement on the shared +catalog instance. Override `catalog_contents_attach_independent = False` when +metadata differs between attachments; the read-only base class otherwise +permits reuse across clients. New DuckDB 2.0 capability fields and mandatory +execution checks remain part of the combined protocol design. + ### SchemaInfo Information about a schema in a catalog: diff --git a/docs/catalog_publication.md b/docs/catalog_publication.md index 1cca3f13..5dd62735 100644 --- a/docs/catalog_publication.md +++ b/docs/catalog_publication.md @@ -1,6 +1,6 @@ # Batched catalog contents and reusable publications -VGI wire revision **2.2.0** retains the `vgi.v2` route and replaces +VGI wire revision **3.0.0** uses the `vgi.v3` route and retains `catalog_contents` with six catalog-wide Arrow IPC streams: schemas, tables, views, functions, macros and indexes. Each stream can contain multiple batches. Null means an empty category. Function and macro subtype fields remain intact. @@ -20,8 +20,8 @@ the argument is discarded. Use `CatalogAttachResult.attach_opaque_data` for subsequent requests. Worker functions still receive their attachment state. Every minor wire revision is an explicit contract boundary. The server checks -major and minor exactly, so clients and workers must both use 2.2. A patch bump -does not permit schema changes. Serving 2.1 and 2.2 during migration requires +major and minor exactly, so clients and workers must both use 3.0. A patch bump +does not permit schema changes. Serving v2 and v3 during migration requires separate deployment URLs or an explicit compatibility implementation. ## Enable publication @@ -40,7 +40,7 @@ S3 client and its SDK when using the S3 repository. The catalog implementation uses APIs available in published **vgi-rpc 0.50.0**. An isolated check against that release passed 157 functional catalog, client, serialization, binary dictionary and publication tests. The package retains its -declared RPC minimum of 0.52.0 for Identity.v2 support; catalog publication does +declared RPC minimum of 0.52.3 for complete request framing; catalog publication does not introduce a dependency on unpublished RPC APIs. ```python @@ -169,3 +169,8 @@ server at a predictable port; `/stats` reports uploads and object GETs. Its CORS headers and preflight support permit browser conformance tests. This fixture is intended for local testing and its object storage lasts only for the worker's lifetime. + +Snapshot manifests and cache keys use `vgi.v3` / wire revision `3.0` and the +current catalog contract hash. The `vgi.catalog.contents.2.2` digest domain +continues to identify the unchanged six-stream codec, independently of the +protocol major. Existing v2 snapshots cannot be reused under the v3 identity. diff --git a/docs/design/reporting-protocols/python-contracts.md b/docs/design/reporting-protocols/python-contracts.md new file mode 100644 index 00000000..a505a180 --- /dev/null +++ b/docs/design/reporting-protocols/python-contracts.md @@ -0,0 +1,27 @@ +# Python reporting contracts + + + +The importable definitions live in `vgi/reporting/`. Each protocol has its own source module and generated reference page. Service implementations and the SQL adapter remain separate work. + +Methods take direct named arguments, with dataclasses for structured values such as report envelopes, schedules and notifications. Scalar arguments are individual RPC columns; dataclass arguments and unary dataclass results use the framework's binary IPC encoding. Producer row schemas and safe SQL reads are explicit annotations. + +| Reference | Python source | +| --- | --- | +| [Shared records](reference/common.md) | `vgi/reporting/common.py` | +| [Portable sources](reference/sources.md) | `vgi/reporting/sources.py` | +| [Bounded discovery](reference/discovery.md) | `vgi/reporting/discovery.py` | +| [Resource identity checks](reference/resource_identity.md) | `vgi/reporting/resource_identity.py` | +| [Objects](reference/objects.md) | `vgi/reporting/objects.py` | +| [Resource ownership discovery](reference/resource_ownership.md) | `vgi/reporting/resource_ownership.py` | +| [Object schedules](reference/schedules_v2.md) | `vgi/reporting/schedules_v2.py` | +| [Reports](reference/reports.md) | `vgi/reporting/reports.py` | +| [Report ownership discovery (optional)](reference/ownership.md) | `vgi/reporting/ownership.py` | +| [Rendering](reference/render.md) | `vgi/reporting/render.py` | +| [Schedules](reference/schedules.md) | `vgi/reporting/schedules.py` | +| [SQL tasks](reference/sql_tasks.md) | `vgi/reporting/sql_tasks.py` | +| [Alerts](reference/alerts.md) | `vgi/reporting/alerts.py` | +| [Notifications](reference/notify.md) | `vgi/reporting/notify.py` | +| [Delegations](reference/delegations.md) | `vgi/reporting/delegations.py` | + +See [wire behavior](README.md#shared-conventions) for lifecycle, errors, encoding rules and worker policy. Regenerate these pages with `uv run python scripts/regen_generated.py --only vgi-python`. diff --git a/docs/design/reporting-protocols/reference/alerts.md b/docs/design/reporting-protocols/reference/alerts.md new file mode 100644 index 00000000..486160cf --- /dev/null +++ b/docs/design/reporting-protocols/reference/alerts.md @@ -0,0 +1,500 @@ +# Alerts: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/alerts.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../alerts.md) · [Shared records](common.md) + +Protocol `vgi.alerts.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `cancel_test_rule` | `job_id`, `expected_version`, `request_id` | `AlertPreviewJob` | `result: binary` (IPC record) | — | +| `create_rule` | `request_id`, `rule`, `ownership`, `execution` | `RuleRecord` | `result: binary` (IPC record) | — | +| `delete_rule` | `rule_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `get_alerts_info` | — | `AlertsInfo` | `result: binary` (IPC record) | Function; table `info` | +| `get_instance` | `instance_id` | `Instance` | `result: binary` (IPC record) | Function | +| `get_rule` | `rule_id` | `RuleRecord` | `result: binary` (IPC record) | Function | +| `get_snooze` | `snooze_id` | `SnoozeRecord` | `result: binary` (IPC record) | Function | +| `get_test_rule` | `job_id` | `AlertPreviewJob` | `result: binary` (IPC record) | Function | +| `list_event_destinations` | `event_id`, `limit` | `DestinationResult` | Producer batches | Function | +| `list_events` | `rule_id`, `instance_id`, `since`, `before`, `limit` | `AlertEvent` | Producer batches | Function; table `events` | +| `list_instances` | `rule_id`, `state`, `since`, `before`, `limit` | `InstanceSummary` | Producer batches | Function; table `instances` | +| `list_rules` | `query`, `ownership_scope`, `state`, `owner`, `parent_owner`, `execution_principal_id`, `enabled`, `limit`, `order_by` | `RuleSummary` | Producer batches | Function; table `rules` | +| `list_snoozes` | `rule_id`, `instance_id`, `active_only`, `limit` | `SnoozeRecord` | Producer batches | Function | +| `list_subscriptions` | `rule_id`, `limit` | `Subscription` | Producer batches | Function | +| `scan_rules` | `query`, `ownership_scope`, `state`, `owner`, `parent_owner`, `execution_principal_id`, `enabled`, `limit`, `order_by` | `RuleRecord` | Producer batches | Function | +| `set_acknowledged` | `instance_id`, `expected_version`, `request_id`, `acknowledged`, `note` | `Instance` | `result: binary` (IPC record) | — | +| `set_execution_principal` | `rule_id`, `expected_version`, `request_id`, `principal_id` | `RuleRecord` | `result: binary` (IPC record) | — | +| `set_ownership` | `rule_id`, `expected_version`, `request_id`, `ownership` | `RuleRecord` | `result: binary` (IPC record) | — | +| `snooze` | `rule_id`, `instance_id`, `expected_version`, `request_id`, `until`, `until_resolved`, `reason` | `SnoozeRecord` | `result: binary` (IPC record) | — | +| `start_test_rule` | `request_id`, `rule`, `execution`, `as_of` | `AlertPreviewJob` | `result: binary` (IPC record) | — | +| `subscribe` | `rule_id`, `request_id`, `destinations` | `Subscription` | `result: binary` (IPC record) | — | +| `unsnooze` | `snooze_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `unsubscribe` | `subscription_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `update_rule` | `rule_id`, `expected_version`, `request_id`, `rule`, `reset_keys` | `RuleRecord` | `result: binary` (IPC record) | — | +| `validate_rule` | `rule`, `execution` | `ValidationResult` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.alerts.v1 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Literal, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import Instant, Json, RowIpc, non_null_list +from vgi.reporting._enums import AlertEventKind, AlertInstanceState, AlertRuleState, Severity +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import ( + Ack, + CredentialStatus, + Destination, + DestinationResult, + ExecutionIdentity, + ExecutionInput, + Notification, + OwnerKey, + Ownership, + OwnershipCapabilities, + OwnershipInput, + OwnershipScope, + ParamValue, + PreviewJob, + PrincipalRef, + ResourceMeta, + RetryPolicy, + RunError, + ServiceInfo, + ValidationResult, +) +from vgi.reporting.sources import QuerySource, ResolvedSource + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AlertMessages(ArrowSerializableDataclass): + """Portable alert templates and highlighted detail columns.""" + + title_template: str = "{{rule.title}}: {{instance.key}}" + summary_template: str = "" + resolved_template: str = "" + field_columns: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AlertRule(ArrowSerializableDataclass): + """Read-only alert query, identity keys, timing and disclosure settings.""" + + title: str + description: str = "" + severity: Severity = "warning" + source: QuerySource + key_columns: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] = field( + default_factory=list + ) + evaluate_every_seconds: int = 300 + pending_for_seconds: int = 0 + resolve_after_seconds: int = 0 + repeat_every_seconds: int = 0 + max_instances: int = 100 + messages: AlertMessages = field(default_factory=AlertMessages) + owner_destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))] = field( + default_factory=list + ) + share_details_with_subscribers: bool = False + enabled: bool = True + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RuleRecord(ResourceMeta): + """Versioned rule, execution identity, generation and evaluation state.""" + + rule_id: str + definition: AlertRule + execution_identity: ExecutionIdentity + key_generation: int + evaluated_source: ResolvedSource | None = None + state: AlertRuleState + state_since: Instant + last_evaluated_at: Instant | None + next_evaluation_at: Instant | None + pending_count: int + firing_count: int + resolving_count: int + last_error: RunError | None + credentials: Annotated[list[CredentialStatus], non_null_list(pa.struct(CredentialStatus.ARROW_SCHEMA))] + disabled_reason: str | None + details_shared_at: Instant | None + details_shared_by: PrincipalRef | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RuleSummary(ResourceMeta): + """Rule state for browsing without query definitions or destinations.""" + + rule_id: str + title: str + enabled: bool + severity: Severity + execution_identity: ExecutionIdentity + state: AlertRuleState + last_evaluated_at: Instant | None + next_evaluation_at: Instant | None + pending_count: int + firing_count: int + resolving_count: int + last_error: RunError | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class InstanceSummary(ArrowSerializableDataclass): + """Alert state, key and acknowledgement metadata without detail IPC.""" + + instance_id: str + rule_id: str + version: int + key_generation: int + evaluated_source: ResolvedSource | None = None + canonical_key: Json + state: AlertInstanceState + severity: Severity + first_seen_at: Instant + last_seen_at: Instant + fired_at: Instant | None + resolved_at: Instant | None + updated_at: Instant + acknowledged: bool + acknowledged_at: Instant | None + acknowledged_by: PrincipalRef | None + acknowledgement_note: str | None + allowed_actions: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Instance(InstanceSummary): + """Authorized detail fields; inaccessible details are null, never empty rows.""" + + latest_details: RowIpc | None + fired_details: RowIpc | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Snooze(ArrowSerializableDataclass): + """Bounded rule or instance silence; evaluation continues.""" + + snooze_id: str + version: int + rule_id: str + instance_id: str | None + until: Instant + until_resolved: bool + reason: str + created_at: Instant + created_by: PrincipalRef + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SnoozeRecord(Snooze): + """Recoverable silence state and version for another authorized client.""" + + state: Annotated[Literal["active", "expired", "cancelled", "resolved"], ArrowType(pa.string())] + ended_at: Instant | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Subscription(ArrowSerializableDataclass): + """Caller-owned destinations subscribed to one alert rule.""" + + subscription_id: str + version: int + rule_id: str + principal: PrincipalRef + destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))] + created_at: Instant + updated_at: Instant + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AlertEvent(ArrowSerializableDataclass): + """Persisted transition, delivery, action or evaluation failure.""" + + event_id: str + rule_id: str + instance_id: str | None + key_generation: int + evaluated_source: ResolvedSource | None = None + occurred_at: Instant + kind: AlertEventKind + actor: PrincipalRef | None + note: str + from_state: str | None + to_state: str | None + error: RunError | None + snooze_id: str | None = None + destination_count: int = 0 + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AlertTestInstance(ArrowSerializableDataclass): + """Prospective instance and message from a read-only test.""" + + canonical_key: Json + details: RowIpc + notification: Notification + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AlertTest(ArrowSerializableDataclass): + """Read-only test results at a specified evaluation instant.""" + + evaluated_at: Instant + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] + instances: Annotated[list[AlertTestInstance], non_null_list(pa.struct(AlertTestInstance.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AlertPreviewJob(PreviewJob): + """Typed bounded rule preview; populated exactly on success.""" + + result: AlertTest | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AlertsInfo(ServiceInfo): + """Alert limits, relative parameters, clock version and retry policy.""" + + ownership: OwnershipCapabilities + relative_tokens: Annotated[list[str], non_null_list(pa.string())] + tzdb_version: str + retry_policy: RetryPolicy + + +class AlertsProtocol(Protocol): + """Proposed vgi.alerts.v1 records and RPC interface; implementations supply all methods. + + OwnershipCapabilities defines all list/scan scope semantics. Exact owner + cannot be combined with a non-all ownership_scope. parent_owner matches + only the recorded direct management parent; folder recursion never expands + directory ancestry. Filters select authorized resources and never grant access. + """ + + protocol_name: ClassVar[str] = "vgi.alerts.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read(table="info") + def get_alerts_info(self) -> AlertsInfo: + """Minimum evaluation interval, max_instances ceiling, maximum snooze, limits.""" + ... + + @sql_read(row_type=RuleSummary, table="rules") + def list_rules( + self, + query: str = "", + ownership_scope: OwnershipScope = "all", + state: str = "", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + execution_principal_id: str | None = None, + enabled: bool | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """AND-filtered summaries; query is title substring, orders id/title/updated_desc with rule_id ties. + + a non-all ownership_scope cannot be combined with owner. limit counts the entire snapshot stream. + """ + ... + + @sql_read(row_type=RuleRecord) + def scan_rules( + self, + query: str = "", + ownership_scope: OwnershipScope = "all", + state: str = "", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + execution_principal_id: str | None = None, + enabled: bool | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Full rule definitions using the same authorization, filters and ordering as list_rules.""" + ... + + @sql_read() + def get_rule(self, rule_id: str) -> RuleRecord: + """Rules with their state (ok, firing, error, disabled) and instance counts.""" + ... + + def create_rule( + self, + request_id: str, + rule: AlertRule, + ownership: OwnershipInput | None = None, + execution: ExecutionInput | None = None, + ) -> RuleRecord: + """Create rule. + + Ownership and execution identity are independently resolved by the host; grant_required if the execution + principal lacks a grant for a required location. + """ + ... + + def update_rule( + self, rule_id: str, expected_version: int, request_id: str, rule: AlertRule, reset_keys: bool = False + ) -> RuleRecord: + """Includes pausing via enabled. + + Changing ordered key columns or explicitly setting reset_keys increments key_generation; old instances + close as superseded atomically. A followed source cannot change key types within a generation. + Source edits alone never clear acknowledgements or snoozes. + """ + ... + + def delete_rule(self, rule_id: str, expected_version: int, request_id: str) -> Ack: + """Instances close as superseded.""" + ... + + def validate_rule(self, rule: AlertRule, execution: ExecutionInput | None = None) -> ValidationResult: + """Check structure and policy without evaluating user SQL; incomplete checks cannot return valid.""" + ... + + def start_test_rule( + self, request_id: str, rule: AlertRule, *, execution: ExecutionInput | None = None, as_of: Instant + ) -> AlertPreviewJob: + """Admit a bounded read-only preview without adopting sources, changing instances or sending messages.""" + ... + + @sql_read() + def get_test_rule(self, job_id: str) -> AlertPreviewJob: + """Read caller-authorized preview status/results until expiry.""" + ... + + def cancel_test_rule(self, job_id: str, expected_version: int, request_id: str) -> AlertPreviewJob: + """Request cancellation; nonterminal status does not confirm cessation.""" + ... + + @sql_read(row_type=InstanceSummary, table="instances") + def list_instances( + self, + rule_id: str = "", + state: str = "", + since: Instant | None = None, + before: Instant | None = None, + limit: int | None = None, + ) -> Stream[ProducerState]: + """Summaries without detail IPC; updated_at range is [since, before), ordered by instance_id.""" + ... + + @sql_read() + def get_instance(self, instance_id: str) -> Instance: + """Instances with state and times; detail values only for callers who may see them.""" + ... + + def set_acknowledged( + self, instance_id: str, expected_version: int, request_id: str, acknowledged: bool, note: str = "" + ) -> Instance: + """Set acknowledged.""" + ... + + def snooze( + self, + rule_id: str, + *, + instance_id: str | None = None, + expected_version: int, + request_id: str, + until: Instant, + until_resolved: bool = False, + reason: str = "", + ) -> SnoozeRecord: + """Null instance_id snoozes the rule.""" + ... + + def unsnooze(self, snooze_id: str, expected_version: int, request_id: str) -> Ack: + """Cancel the identified snooze using the version returned by get_snooze or list_snoozes.""" + ... + + def subscribe( + self, + rule_id: str, + request_id: str, + destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))], + ) -> Subscription: + """Subscribe.""" + ... + + def unsubscribe(self, subscription_id: str, expected_version: int, request_id: str) -> Ack: + """Unsubscribe.""" + ... + + @sql_read(row_type=Subscription) + def list_subscriptions(self, rule_id: str, limit: int | None = None) -> Stream[ProducerState]: + """List subscriptions.""" + ... + + @sql_read(row_type=AlertEvent, table="events") + def list_events( + self, + rule_id: str = "", + instance_id: str = "", + since: Instant | None = None, + before: Instant | None = None, + limit: int | None = None, + ) -> Stream[ProducerState]: + """Timeline ordered by (occurred_at, event_id), with [since, before) time bounds and total limit.""" + ... + + def set_ownership(self, rule_id: str, expected_version: int, request_id: str, ownership: Ownership) -> RuleRecord: + """Transfer management; preserve data and execution authority.""" + ... + + def set_execution_principal( + self, rule_id: str, expected_version: int, request_id: str, principal_id: str + ) -> RuleRecord: + """Select a separately authorized execution identity for future work.""" + ... + + @sql_read() + def get_snooze(self, snooze_id: str) -> SnoozeRecord: + """Authorize the parent and return the current ID/version, scope, end condition and state.""" + ... + + @sql_read(row_type=SnoozeRecord) + def list_snoozes( + self, + rule_id: str, + instance_id: str | None = None, + active_only: bool = True, + limit: int | None = None, + ) -> Stream[ProducerState]: + """Without an instance filter return rule and instance snoozes; otherwise include rule-wide and that instance. + + Validate instance membership and parent access; order by snooze_id and + recheck authorization on continuation. Returned versions permit unsnooze. + """ + ... + + @sql_read(row_type=DestinationResult) + def list_event_destinations(self, event_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized event delivery outcomes in original dispatch order.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/common.md b/docs/design/reporting-protocols/reference/common.md new file mode 100644 index 00000000..d39b7891 --- /dev/null +++ b/docs/design/reporting-protocols/reference/common.md @@ -0,0 +1,690 @@ +# Shared records: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/common.py`. Edit the Python module and regenerate this page. + +## Arrow aliases and string choices + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Explicit Arrow encodings shared by the proposed reporting contracts.""" + +from datetime import datetime +from typing import Annotated + +import pyarrow as pa +from vgi_rpc import ArrowType + +Instant = Annotated[datetime, ArrowType(pa.timestamp("us", "UTC"))] +"""An aware datetime, encoded as UTC microseconds on the wire.""" + +Json = str +"""One JSON value encoded as text; semantic validation belongs to the worker.""" + +SchemaIpc = Annotated[pa.Schema, ArrowType(pa.binary())] +"""An Arrow schema serialized as one IPC schema message.""" + +RowIpc = Annotated[pa.RecordBatch, ArrowType(pa.binary())] +"""A one-row batch serialized with its schema as a complete IPC stream.""" + + +def non_null_list(item: pa.DataType) -> ArrowType: + """Declare a list whose elements cannot be null, independent of Arrow defaults.""" + return ArrowType(pa.list_(pa.field("item", item, nullable=False))) +``` + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Known string choices, encoded as utf8 rather than dictionary-encoded Enum. + +Literal annotations assist callers and reviewers. Workers enforce allowed input +values; clients must still tolerate unknown output values from newer workers. +Plain strings are retained for explicitly open vendor kinds and action hints. +""" + +from typing import Annotated, Literal + +import pyarrow as pa +from vgi_rpc import ArrowType + +ExecutionState = Annotated[Literal["ready", "needs_credentials", "unavailable"], ArrowType(pa.string())] +CredentialState = Annotated[Literal["ok", "expiring", "expired", "missing"], ArrowType(pa.string())] +LimitUnit = Annotated[Literal["bytes", "rows", "seconds", "count", "utf8_bytes"], ArrowType(pa.string())] +ParameterType = Annotated[ + Literal["text", "number", "date", "boolean", "select", "multi_select", "date_range"], ArrowType(pa.string()) +] +ParamKind = Annotated[Literal["literal", "relative"], ArrowType(pa.string())] +Severity = Annotated[Literal["info", "warning", "critical"], ArrowType(pa.string())] +DeliveryStatus = Annotated[Literal["accepted", "refused", "failed", "unknown"], ArrowType(pa.string())] +TriggerKind = Annotated[Literal["cron", "once"], ArrowType(pa.string())] +EffectOutcome = Annotated[Literal["none", "committed", "rolled_back", "partial", "unknown"], ArrowType(pa.string())] +ResolvedOutcome = Annotated[Literal["committed", "rolled_back", "partial"], ArrowType(pa.string())] +StepStatus = Annotated[Literal["pending", "running", "succeeded", "failed", "skipped"], ArrowType(pa.string())] +RecoveryState = Annotated[ + Literal[ + "none", "retry_scheduled", "retryable", "needs_reauthorization", "needs_resolution", "exhausted", "resolved" + ], + ArrowType(pa.string()), +] +RunStatus = Annotated[ + Literal["pending", "running", "retry_wait", "succeeded", "skipped", "failed", "cancelled"], ArrowType(pa.string()) +] +RunTriggerKind = Annotated[Literal["schedule", "manual", "test"], ArrowType(pa.string())] +RevisionKind = Annotated[Literal["edit", "agent", "restore", "import"], ArrowType(pa.string())] +RenderStatus = Annotated[Literal["queued", "running", "succeeded", "failed", "cancelled"], ArrowType(pa.string())] +ReportTracking = Annotated[Literal["pinned", "published", "head"], ArrowType(pa.string())] +OutputFormat = Annotated[Literal["none", "arrow", "parquet", "csv"], ArrowType(pa.string())] +InlineMode = Annotated[Literal["none", "summary", "image", "table", "report"], ArrowType(pa.string())] +TargetKind = Annotated[Literal["catalog", "host"], ArrowType(pa.string())] +LoadMode = Annotated[Literal["replace", "append", "merge", "snapshot"], ArrowType(pa.string())] +SchemaChangePolicy = Annotated[Literal["fail", "add_columns"], ArrowType(pa.string())] +TransactionMode = Annotated[Literal["single", "per_statement", "none"], ArrowType(pa.string())] +IncrementalKind = Annotated[Literal["none", "derived", "stored"], ArrowType(pa.string())] +HealthState = Annotated[Literal["healthy", "failing", "stale", "paused", "disabled", "new"], ArrowType(pa.string())] +AlertRuleState = Annotated[Literal["ok", "firing", "error", "disabled"], ArrowType(pa.string())] +AlertInstanceState = Annotated[ + Literal["pending", "firing", "resolving", "resolved", "superseded"], ArrowType(pa.string()) +] +AlertEventKind = Annotated[ + Literal[ + "transition", + "notification", + "acknowledged", + "unacknowledged", + "snoozed", + "unsnoozed", + "subscribed", + "unsubscribed", + "rule_error", + "rule_recovered", + "superseded", + ], + ArrowType(pa.string()), +] +DelegationKind = Annotated[Literal["catalog", "service"], ArrowType(pa.string())] +``` + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Shared reporting records. + +These are importable protocol definitions, not a service implementation. +Every call runs within one tenant selected and authorized by trusted host +authentication/routing, including single-tenant installations. Resource IDs, +owner IDs and principal IDs are opaque, stable, non-recycled host-canonical +identifiers that disambiguate tenants and identity issuers. Bare provider-local +IDs are insufficient when they can collide. Keys never select or switch the +authenticated tenant; foreign-tenant resources and identities are refused. + +Caches, request-id replay records, jobs and continuations are bound to the host, +authenticated tenant and caller. Reauthentication must preserve that binding; +a continuation cannot preserve revoked access. Workers evaluate current policy +and authoritative effective membership without requiring directory enumeration. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, Literal + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType + +from vgi.reporting._arrow import Instant, Json, non_null_list +from vgi.reporting._enums import ( + CredentialState, + DelegationKind, + DeliveryStatus, + EffectOutcome, + ExecutionState, + LimitUnit, + ParameterType, + ParamKind, + RecoveryState, + RenderStatus, + ResolvedOutcome, + RunStatus, + RunTriggerKind, + Severity, + StepStatus, + TriggerKind, +) + +OwnershipScope = Annotated[Literal["all", "self", "member", "self_or_member"], ArrowType(pa.string())] + +# Teams use group identities. One directory entity has one canonical kind/id, +# never interchangeable team/group aliases. Extensions use .. +STANDARD_OWNER_KINDS = ("principal", "group", "organization", "project", "workspace") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PrincipalRef(ArrowSerializableDataclass): + """Canonical human or service actor; group owners are never execution actors. + + Labels and email are display metadata, never authority. The host resolves + issuer/tenant mappings and eligibility; kind alone grants no permission. + """ + + id: str + kind: Annotated[Literal["user", "service"], ArrowType(pa.string())] + display_name: str + email: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnerKey(ArrowSerializableDataclass): + """Host-canonical management identity, matched by exact (kind, id). + + Standard kinds are principal, group (including teams), organization, + project and workspace; advertised extensions use .. Principal + owner IDs equal the corresponding PrincipalRef.id, for user or service + actors. No key implies membership, a role, or permission to act as its owner. + """ + + kind: str + id: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnerRef(OwnerKey): + """Canonical owner and display label; workers never authorize supplied labels.""" + + display_name: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Ownership(ArrowSerializableDataclass): + """One direct owner and one durable management parent in the authorized tenant. + + parent_owner_ref is the recorded direct authority, not the full organization + hierarchy, directory membership or folder containment. A principal owner + requires a non-principal durable parent; a root organizational owner may + have no parent. Parent changes require explicit authorized ownership changes. + + Ownership never grants execution consent or credentials. Transfers preserve + execution identity, but workers revalidate resource-bound execution consent + after a transfer and before dispatch, disabling work if it is no longer valid. + """ + + owner_ref: OwnerRef + parent_owner_ref: OwnerRef | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnershipCapabilities(ArrowSerializableDataclass): + """Finite ownership vocabulary and collection filters supported by a worker. + + Service-info policy applies to every listed resource kind, including folders; + context-specific identity capabilities can narrow the assignable kinds. + all/self are required scopes. member/self_or_member are optional as a pair, + requiring nonempty membership_owner_kinds, a subset of non-principal + owner_kinds. An empty list disables membership filtering, not exact checks. + + all selects every currently readable match; self selects resources directly + owned by the authenticated principal (user or service). member selects only + resources whose direct owner is an advertised membership kind and for which + authoritative effective membership holds, including nested groups according + to the identity authority's rules. Parent membership, authorship, delegated + administration and ACL visibility alone never satisfy member. self_or_member + is the deduplicated union. Every selection still requires resource access. + + Membership is evaluated server-side without client-supplied membership lists. + Unsupported scopes fail UNIMPLEMENTED; an unavailable/incomplete membership + decision fails UNAVAILABLE rather than returning empty or broadened results. + """ + + owner_kinds: Annotated[list[str], non_null_list(pa.string())] + parent_owner_kinds: Annotated[list[str], non_null_list(pa.string())] + scopes: Annotated[list[OwnershipScope], non_null_list(pa.string())] + membership_owner_kinds: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ResourceMeta(ArrowSerializableDataclass): + """Versioned ownership and attribution shared by persistent resources. + + allowed_actions is a caller-specific hint, never a grant. Worker policy + independently authorizes reads, edits, transfers and execution using current + membership/roles; membership does not imply every action. Folder recursion + and placement never rewrite explicit ownership or its management parent. + """ + + version: int + ownership: Ownership + created_at: Instant + created_by: PrincipalRef + updated_at: Instant + updated_by: PrincipalRef + allowed_actions: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnershipInput(ArrowSerializableDataclass): + """Creation-time preferences; None requests defaults in the authenticated tenant. + + Workers resolve canonical keys and labels and authorize the complete owner + and parent assignment. Supplied display metadata cannot confer authority. + """ + + owner_ref: OwnerRef | None = None + parent_owner_ref: OwnerRef | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ExecutionInput(ArrowSerializableDataclass): + """Canonical user/service execution ID; None selects the authenticated caller. + + Team/group ownership never chooses an execution actor. The host must resolve + and separately authorize the actor, including consent and credential use. + """ + + principal_id: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ExecutionIdentity(ArrowSerializableDataclass): + """Resolved execution principal and its current readiness.""" + + principal: PrincipalRef + state: ExecutionState + reason: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Ack(ArrowSerializableDataclass): + """Acknowledgement of an admitted mutation.""" + + applied: bool + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Limit(ArrowSerializableDataclass): + """Worker-chosen limit; None means unlimited and zero means zero.""" + + name: str + value: int | None + unit: LimitUnit + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ServiceInfo(ArrowSerializableDataclass): + """Common advertised protocol identity, writability and limits.""" + + protocol_version: str + display_name: str + writable: bool + limits: Annotated[list[Limit], non_null_list(pa.struct(Limit.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RetryPolicy(ArrowSerializableDataclass): + """Worker retry budgets and backoff bounds; attempts include the first dispatch.""" + + max_attempts: int + max_elapsed_seconds: int + initial_backoff_seconds: int + max_backoff_seconds: int + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DataSource(ArrowSerializableDataclass): + """Non-secret session attachment reference with an explicit remote catalog.""" + + alias: str + attachment_id: str + location: str + catalog_name: str + label: str = "" + required: bool = True + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DelegationKey(ArrowSerializableDataclass): + """Exact catalog attachment or standalone service credential key.""" + + kind: DelegationKind + location: str + catalog_name: str + attachment_id: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Delegation(DelegationKey): + """Write-only grant and ticket; None expiry means unlimited.""" + + grant: str + ticket: str + expires_at: Instant | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DelegationWrite(ArrowSerializableDataclass): + """Compare-and-swap credential write; version zero requires an absent key.""" + + delegation: Delegation + expected_version: int + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DelegationRecord(DelegationKey): + """Readable delegation metadata; never includes a grant or ticket.""" + + version: int + expires_at: Instant | None + updated_at: Instant + + +@dataclass(frozen=True, slots=True, kw_only=True) +class CredentialStatus(DelegationKey): + """Readiness of one required delegation without its credentials.""" + + state: CredentialState + expires_at: Instant | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ParameterOption(ArrowSerializableDataclass): + """One display label and JSON scalar accepted by a select parameter.""" + + label: str + value_json: Json + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ParameterSpec(ArrowSerializableDataclass): + """Public parameter type, default and bounded static select values. + + Workers advertise and enforce max_parameter_options; dynamic directory/data + selectors are not encoded as unbounded option arrays. + """ + + key: str + label: str + type: ParameterType + required: bool = False + default_json: Json = "null" + options: Annotated[list[ParameterOption], non_null_list(pa.struct(ParameterOption.ARROW_SCHEMA))] = field( + default_factory=list + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ParamValue(ArrowSerializableDataclass): + """Tagged literal JSON or relative period; exactly one payload is present.""" + + key: str + kind: ParamKind + json: Json | None = None + relative: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ReportRef(ArrowSerializableDataclass): + """Report service and ID, with an optional pinned revision.""" + + service_url: str + report_id: str + revision_id: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Destination(ArrowSerializableDataclass): + """Channel kind and address interpreted by the notify worker.""" + + kind: str + address: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Field(ArrowSerializableDataclass): + """A display label/value pair, also used for notification correlation entries.""" + + label: str + value: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Attachment(ArrowSerializableDataclass): + """Immutable attachment content and a service-fetchable download URL.""" + + filename: str + media_type: str + url: str + size_bytes: int + sha256: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Artifact(Attachment): + """Produced attachment with the expiry of its temporary URL.""" + + expires_at: Instant | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Output(ArrowSerializableDataclass): + """Action output, optionally with a downloadable artifact and known row count.""" + + artifact: Artifact | None + media_type: str + row_count: int | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RunError(ArrowSerializableDataclass): + """Sanitized persisted error; canonical code and stable application kind.""" + + code: str + kind: str + location: str | None + message: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DestinationResult(ArrowSerializableDataclass): + """One destination outcome, including ambiguous provider acceptance.""" + + destination: Destination + status: DeliveryStatus + reason: str | None + message: str | None + provider_reference: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Notification(ArrowSerializableDataclass): + """Portable message content with explicit attribution and correlation.""" + + title: str + summary: str + body_markdown: str = "" + body_html: Attachment | None = None + link_url: str = "" + link_label: str = "" + fields: Annotated[list[Field], non_null_list(pa.struct(Field.ARROW_SCHEMA))] = field(default_factory=list) + image: Attachment | None = None + attachments: Annotated[list[Attachment], non_null_list(pa.struct(Attachment.ARROW_SCHEMA))] = field( + default_factory=list + ) + severity: Severity = "info" + thread_key: str = "" + on_behalf_of: PrincipalRef + correlation: Annotated[list[Field], non_null_list(pa.struct(Field.ARROW_SCHEMA))] = field(default_factory=list) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Trigger(ArrowSerializableDataclass): + """Cron or once trigger; instants are aware UTC datetimes on the wire.""" + + kind: TriggerKind + cron: str | None = None + run_at: Instant | None = None + time_zone: str = "UTC" + start_at: Instant | None = None + end_at: Instant | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TriggerPreview(ArrowSerializableDataclass): + """Deterministic fire instants and the worker time-zone database version.""" + + fire_times: Annotated[list[Instant], non_null_list(pa.timestamp("us", "UTC"))] + description: str + warnings: Annotated[list[str], non_null_list(pa.string())] + tzdb_version: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StepAttempt(ArrowSerializableDataclass): + """Immutable observed attempt timing, effect outcome and failure.""" + + attempt: int + started_at: Instant + finished_at: Instant | None + effect_outcome: EffectOutcome + error: RunError | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RunStep(ArrowSerializableDataclass): + """Step status and last attempt; full attempt history is a separate stream.""" + + step_index: int + operation_id: str + kind: str + status: StepStatus + effect_outcome: EffectOutcome + attempt_count: int + last_attempt: StepAttempt | None + rows_affected: int | None + receiver_reference: str | None + deduplication_expires_at: Instant | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Recovery(ArrowSerializableDataclass): + """Current recovery state, retry timing and actions permitted for the caller.""" + + state: RecoveryState + failed_step_index: int | None + next_retry_at: Instant | None + attempts_remaining: int + reason: str | None + allowed_actions: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StepResolution(ArrowSerializableDataclass): + """Outcome and receipt/audit evidence to be verified by the worker.""" + + step_index: int + outcome: ResolvedOutcome + evidence_ref: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RecoveryEvent(ArrowSerializableDataclass): + """Audit record for an authorized recovery action.""" + + occurred_at: Instant + actor: PrincipalRef + action: str + note: str + resolutions: Annotated[list[StepResolution], non_null_list(pa.struct(StepResolution.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RunSummary(ArrowSerializableDataclass): + """Bounded run status; step attempts and recovery history are streamed reads.""" + + run_id: str + version: int + trigger_kind: RunTriggerKind + scheduled_for: Instant + admitted_at: Instant + started_at: Instant | None + finished_at: Instant | None + status: RunStatus + skip_reason: str | None + execution_principal: PrincipalRef + resource_version: int + error: RunError | None + recovery: Recovery + allowed_actions: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Run(RunSummary): + """Frozen run inputs and bounded outputs, without nested histories. + + list_run_steps, list_step_attempts, list_recovery_events and + list_run_destinations expose independently consumable history rows. + """ + + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] + outputs: Annotated[list[Output], non_null_list(pa.struct(Output.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ValidationIssue(ArrowSerializableDataclass): + """A safe field-specific validation failure or incomplete check.""" + + field: str + code: str + message: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ValidationResult(ArrowSerializableDataclass): + """Advisory validation without user SQL or rendering. + + complete=False means one or more checks could not finish; valid cannot be + true in that case. A valid result never grants future execution authority. + Workers bound issues and report truncation explicitly through complete=False. + """ + + valid: bool + complete: bool + issues: Annotated[list[ValidationIssue], non_null_list(pa.struct(ValidationIssue.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PreviewJob(ArrowSerializableDataclass): + """Versioned, caller-authorized execution preview with a typed child result. + + Starts deduplicate request_id. A worker may complete synchronously. Success + has a result, failure an error, and terminal jobs have zero polling delay. + Cancellation is best effort; results expire at expires_at. Tests preserve + the original read-only/rollback-only restrictions and never deliver messages. + + Preview services advertise finite max_preview_seconds (seconds) and + max_preview_result_bytes (bytes) in their service limits. Exceeding either + fails the job with RESOURCE_EXHAUSTED, never silently truncates a success. + """ + + job_id: str + version: int + status: RenderStatus + created_at: Instant + finished_at: Instant | None + poll_after_seconds: int + expires_at: Instant + error: RunError | None + allowed_actions: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class KeyValue(ArrowSerializableDataclass): + """Lossless tagged scalar used by canonical alert keys and watermark JSON.""" + + type: str + value: str +``` diff --git a/docs/design/reporting-protocols/reference/delegations.md b/docs/design/reporting-protocols/reference/delegations.md new file mode 100644 index 00000000..57640818 --- /dev/null +++ b/docs/design/reporting-protocols/reference/delegations.md @@ -0,0 +1,104 @@ +# Delegations: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/delegations.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../credentials.md) · [Shared records](common.md) + +Protocol `vgi.delegations.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `get_delegation` | `key` | `DelegationRecord` | `result: binary` (IPC record) | Function | +| `get_delegation_limits` | — | `DelegationLimits` | `result: binary` (IPC record) | Function | +| `list_delegations` | `limit` | `DelegationRecord` | Producer batches | Function; table `delegations` | +| `put_delegations` | `request_id`, `delegations` | `DelegationPutResult` | `result: binary` (IPC record) | — | +| `revoke_delegation` | `key`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.delegations.v1 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Annotated, ClassVar, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import non_null_list +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import ( + Ack, + DelegationKey, + DelegationRecord, + DelegationWrite, + Limit, +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DelegationPutResult(ArrowSerializableDataclass): + """Metadata for an atomic credential update, without credential values.""" + + delegations: Annotated[list[DelegationRecord], non_null_list(pa.struct(DelegationRecord.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DelegationLimits(ArrowSerializableDataclass): + """Finite max_delegation_writes count and any additional worker limits.""" + + limits: Annotated[list[Limit], non_null_list(pa.struct(Limit.ARROW_SCHEMA))] + + +class DelegationsProtocol(Protocol): + """Proposed vgi.delegations.v1 records and RPC interface; implementations supply all methods.""" + + protocol_name: ClassVar[str] = "vgi.delegations.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read() + def get_delegation_limits(self) -> DelegationLimits: + """Advertise the atomic write batch ceiling before credentials are submitted.""" + ... + + def put_delegations( + self, + request_id: str, + delegations: Annotated[list[DelegationWrite], non_null_list(pa.struct(DelegationWrite.ARROW_SCHEMA))], + ) -> DelegationPutResult: + """Put delegations. + + Store or replace the caller's delegations, keyed by (kind, location, catalog_name, attachment_id); duplicate + keys in one request are invalid_request. Enforce max_delegation_writes before any write; the batch is atomic. + """ + ... + + @sql_read(row_type=DelegationRecord, table="delegations") + def list_delegations(self, limit: int | None = None) -> Stream[ProducerState]: + """The caller's delegations with expires_at and updated_at. Never grants or tickets.""" + ... + + @sql_read() + def get_delegation(self, key: DelegationKey) -> DelegationRecord: + """Caller-scoped exact metadata/version lookup; NOT_FOUND for an absent key, never secrets.""" + ... + + def revoke_delegation(self, key: DelegationKey, expected_version: int, request_id: str) -> Ack: + """Destroy exactly the caller's matching delegation.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/discovery.md b/docs/design/reporting-protocols/reference/discovery.md new file mode 100644 index 00000000..8b6d4de8 --- /dev/null +++ b/docs/design/reporting-protocols/reference/discovery.md @@ -0,0 +1,364 @@ +# Bounded discovery: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/discovery.py`. Edit the Python module and regenerate this page. + +## Arrow aliases and string choices + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Explicit Arrow encodings shared by the proposed reporting contracts.""" + +from datetime import datetime +from typing import Annotated + +import pyarrow as pa +from vgi_rpc import ArrowType + +Instant = Annotated[datetime, ArrowType(pa.timestamp("us", "UTC"))] +"""An aware datetime, encoded as UTC microseconds on the wire.""" + +Json = str +"""One JSON value encoded as text; semantic validation belongs to the worker.""" + +SchemaIpc = Annotated[pa.Schema, ArrowType(pa.binary())] +"""An Arrow schema serialized as one IPC schema message.""" + +RowIpc = Annotated[pa.RecordBatch, ArrowType(pa.binary())] +"""A one-row batch serialized with its schema as a complete IPC stream.""" + + +def non_null_list(item: pa.DataType) -> ArrowType: + """Declare a list whose elements cannot be null, independent of Arrow defaults.""" + return ArrowType(pa.list_(pa.field("item", item, nullable=False))) +``` + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Known string choices, encoded as utf8 rather than dictionary-encoded Enum. + +Literal annotations assist callers and reviewers. Workers enforce allowed input +values; clients must still tolerate unknown output values from newer workers. +Plain strings are retained for explicitly open vendor kinds and action hints. +""" + +from typing import Annotated, Literal + +import pyarrow as pa +from vgi_rpc import ArrowType + +ExecutionState = Annotated[Literal["ready", "needs_credentials", "unavailable"], ArrowType(pa.string())] +CredentialState = Annotated[Literal["ok", "expiring", "expired", "missing"], ArrowType(pa.string())] +LimitUnit = Annotated[Literal["bytes", "rows", "seconds", "count", "utf8_bytes"], ArrowType(pa.string())] +ParameterType = Annotated[ + Literal["text", "number", "date", "boolean", "select", "multi_select", "date_range"], ArrowType(pa.string()) +] +ParamKind = Annotated[Literal["literal", "relative"], ArrowType(pa.string())] +Severity = Annotated[Literal["info", "warning", "critical"], ArrowType(pa.string())] +DeliveryStatus = Annotated[Literal["accepted", "refused", "failed", "unknown"], ArrowType(pa.string())] +TriggerKind = Annotated[Literal["cron", "once"], ArrowType(pa.string())] +EffectOutcome = Annotated[Literal["none", "committed", "rolled_back", "partial", "unknown"], ArrowType(pa.string())] +ResolvedOutcome = Annotated[Literal["committed", "rolled_back", "partial"], ArrowType(pa.string())] +StepStatus = Annotated[Literal["pending", "running", "succeeded", "failed", "skipped"], ArrowType(pa.string())] +RecoveryState = Annotated[ + Literal[ + "none", "retry_scheduled", "retryable", "needs_reauthorization", "needs_resolution", "exhausted", "resolved" + ], + ArrowType(pa.string()), +] +RunStatus = Annotated[ + Literal["pending", "running", "retry_wait", "succeeded", "skipped", "failed", "cancelled"], ArrowType(pa.string()) +] +RunTriggerKind = Annotated[Literal["schedule", "manual", "test"], ArrowType(pa.string())] +RevisionKind = Annotated[Literal["edit", "agent", "restore", "import"], ArrowType(pa.string())] +RenderStatus = Annotated[Literal["queued", "running", "succeeded", "failed", "cancelled"], ArrowType(pa.string())] +ReportTracking = Annotated[Literal["pinned", "published", "head"], ArrowType(pa.string())] +OutputFormat = Annotated[Literal["none", "arrow", "parquet", "csv"], ArrowType(pa.string())] +InlineMode = Annotated[Literal["none", "summary", "image", "table", "report"], ArrowType(pa.string())] +TargetKind = Annotated[Literal["catalog", "host"], ArrowType(pa.string())] +LoadMode = Annotated[Literal["replace", "append", "merge", "snapshot"], ArrowType(pa.string())] +SchemaChangePolicy = Annotated[Literal["fail", "add_columns"], ArrowType(pa.string())] +TransactionMode = Annotated[Literal["single", "per_statement", "none"], ArrowType(pa.string())] +IncrementalKind = Annotated[Literal["none", "derived", "stored"], ArrowType(pa.string())] +HealthState = Annotated[Literal["healthy", "failing", "stale", "paused", "disabled", "new"], ArrowType(pa.string())] +AlertRuleState = Annotated[Literal["ok", "firing", "error", "disabled"], ArrowType(pa.string())] +AlertInstanceState = Annotated[ + Literal["pending", "firing", "resolving", "resolved", "superseded"], ArrowType(pa.string()) +] +AlertEventKind = Annotated[ + Literal[ + "transition", + "notification", + "acknowledged", + "unacknowledged", + "snoozed", + "unsnoozed", + "subscribed", + "unsubscribed", + "rule_error", + "rule_recovered", + "superseded", + ], + ArrowType(pa.string()), +] +DelegationKind = Annotated[Literal["catalog", "service"], ArrowType(pa.string())] +``` + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Bounded discovery and collection admission shared by reporting workers. + +Search limits count rows across the entire producer stream, not per batch or +continuation. Exact checks do not require search or directory enumeration. +Workers translate validation failures to INVALID_ARGUMENT before admission; +unsupported capabilities use UNIMPLEMENTED and quota refusals RESOURCE_EXHAUSTED. + +Collection reads use a positive total limit, or None for the complete authorized +result. Continuations preserve filters, ordering and the admitted snapshot while +rechecking current access. Quota or snapshot expiry must fail explicitly, never +silently truncate a successful stream. Snapshots need not materialize all rows. +""" + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Literal + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc.errors import Code + +from vgi.reporting._arrow import non_null_list +from vgi.reporting.common import STANDARD_OWNER_KINDS, Limit, OwnerKey, Ownership, OwnershipCapabilities, OwnershipScope + +CheckState = Annotated[Literal["allowed", "denied", "unknown"], ArrowType(pa.string())] + +# Required limit names for the corresponding input collections. A worker may +# choose a smaller finite value, but must advertise it before accepting batches. +BATCH_LIMIT_NAMES = ( + "max_check_destinations", + "max_send_destinations", + "max_delegation_writes", + "max_source_checks", + "max_parameter_options", +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DiscoveryCapabilities(ArrowSerializableDataclass): + """Caller-specific search policy; exact checks remain independently available. + + Empty input is suggestions only, never implicit enumeration. Identifier + schemes apply to exact checks; search matching follows query_hint. + """ + + search_supported: bool = False + suggestions_supported: bool = False + enumeration_supported: bool = False + min_query_length: int = 1 + max_results: int = 20 + query_hint: str = "Supply an exact identifier" + identifier_schemes: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnerDiscoveryCapabilities(DiscoveryCapabilities): + """Exact/search policy plus owner kinds accepted in the authorized resource context. + + Membership filters are independent of search, suggestions and enumeration. + policy narrows service-info ownership capabilities for this context; it + never authorizes querying another tenant or acting as an owner. + """ + + policy: OwnershipCapabilities + + +class MembershipUnavailableError(RuntimeError): + """Authoritative membership is indeterminate; never a negative membership result.""" + + error_code: ClassVar[Code] = Code.UNAVAILABLE + error_kind: ClassVar[str] = "membership_unavailable" + + +def validate_owner_key(key: OwnerKey) -> None: + """Validate key syntax; the host must resolve the opaque ID within the authorized tenant.""" + vendor, separator, name = key.kind.partition(".") + if not key.id.strip() or ( + key.kind not in STANDARD_OWNER_KINDS + and not ( + separator and vendor and name and all(key.kind.split(".")) and not any(char.isspace() for char in key.kind) + ) + ): + raise ValueError("Owner keys require a canonical kind and nonempty ID") + + +def validate_ownership_capabilities(capabilities: OwnershipCapabilities) -> None: + """Reject contradictory or incomplete ownership-filter advertisements.""" + for kinds in (capabilities.owner_kinds, capabilities.parent_owner_kinds, capabilities.membership_owner_kinds): + if len(kinds) != len(set(kinds)): + raise ValueError("Ownership kinds must be unique") + for kind in kinds: + validate_owner_key(OwnerKey(kind=kind, id="capability")) + if not capabilities.owner_kinds or "principal" in capabilities.parent_owner_kinds: + raise ValueError("Ownership requires owner kinds and non-principal parent kinds") + if "principal" in capabilities.owner_kinds and not capabilities.parent_owner_kinds: + raise ValueError("Principal ownership requires a durable parent kind") + scopes = set(capabilities.scopes) + if len(scopes) != len(capabilities.scopes) or scopes not in ( + {"all", "self"}, + {"all", "self", "member", "self_or_member"}, + ): + raise ValueError("Advertise all/self, optionally with both membership scopes") + membership_kinds = set(capabilities.membership_owner_kinds) + if not membership_kinds <= set(capabilities.owner_kinds) or "principal" in membership_kinds: + raise ValueError("Membership kinds must be supported non-principal owner kinds") + if bool(membership_kinds) != ("member" in scopes): + raise ValueError("Membership scopes require explicit membership owner kinds") + + +def validate_ownership_assignment(ownership: Ownership, capabilities: OwnershipCapabilities) -> None: + """Validate a resolved ownership shape; tenant checks and authorization remain worker obligations.""" + validate_ownership_capabilities(capabilities) + owner, parent = ownership.owner_ref, ownership.parent_owner_ref + validate_owner_key(owner) + if owner.kind not in capabilities.owner_kinds: + raise ValueError("Unsupported owner kind") + if parent is None: + if owner.kind == "principal": + raise ValueError("Principal ownership requires a durable parent") + else: + validate_owner_key(parent) + if parent.kind not in capabilities.parent_owner_kinds or (owner.kind, owner.id) == (parent.kind, parent.id): + raise ValueError("Ownership requires a distinct supported durable parent") + + +def matches_ownership_scope( + ownership: Ownership, + *, + principal_id: str, + scope: OwnershipScope, + capabilities: OwnershipCapabilities, + membership_lookup: Callable[[OwnerKey], bool | None] | None = None, +) -> bool: + """Evaluate only an ownership predicate, never resource access or execution authority. + + principal_id must come from the authenticated host context. The callback + must use that same caller/tenant and authoritative effective membership for + one direct owner, without directory enumeration. None means indeterminate; + callback failures propagate. Workers may implement equivalent indexed/set + queries for scale and must separately enforce current authorization. + """ + validate_ownership_capabilities(capabilities) + if not principal_id.strip() or scope not in {"all", "self", "member", "self_or_member"}: + raise ValueError("A canonical authenticated principal and known scope are required") + if scope not in capabilities.scopes: + raise NotImplementedError("Ownership scope is unsupported") + if scope == "all": + return True + owner = ownership.owner_ref + is_self = owner.kind == "principal" and owner.id == principal_id + if scope == "self": + return is_self + if scope == "self_or_member" and is_self: + return True + if owner.kind not in capabilities.membership_owner_kinds: + return False + if membership_lookup is None: + raise MembershipUnavailableError("An authoritative membership decision is required") + decision = membership_lookup(OwnerKey(kind=owner.kind, id=owner.id)) + if type(decision) is not bool: + raise MembershipUnavailableError("Membership could not be determined") + return decision + + +def validate_search(query: str, limit: int, capabilities: DiscoveryCapabilities) -> None: + """Validate a bounded search, distinguishing unsupported search from no matches.""" + if capabilities.min_query_length < 0 or capabilities.max_results < 1: + raise ValueError("Invalid discovery capabilities") + validate_batch_size(limit, maximum=capabilities.max_results, minimum=1) + if not query.strip(): + if not capabilities.suggestions_supported: + raise NotImplementedError("Suggestions are unavailable; supply an exact identifier or search query") + else: + if not capabilities.search_supported: + raise NotImplementedError("Search is unavailable; use an exact identifier check") + if len(query.strip()) < capabilities.min_query_length: + raise ValueError("Search query is too short") + + +def validate_batch_size(size: int, *, maximum: int, minimum: int = 0) -> None: + """Reject invalid sizes; a finite advertised ceiling bounds input materialization.""" + if ( + any(isinstance(value, bool) or not isinstance(value, int) for value in (size, minimum, maximum)) + or minimum < 0 + or maximum < minimum + or not minimum <= size <= maximum + ): + raise ValueError(f"Expected between {minimum} and {maximum} items") + + +def batch_limit(limits: list[Limit], name: str) -> int: + """Read one advertised finite count limit; absence never implies unlimited.""" + matches = [limit for limit in limits if limit.name == name] + if len(matches) != 1: + raise ValueError(f"Expected exactly one advertised {name} limit") + limit = matches[0] + if limit.unit != "count" or isinstance(limit.value, bool) or not isinstance(limit.value, int) or limit.value < 0: + raise ValueError(f"{name} must be a finite nonnegative count") + return limit.value + + +def validate_list_request( + *, + limit: int | None, + order_by: str, + supported_orders: tuple[str, ...], + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + ownership_capabilities: OwnershipCapabilities | None = None, +) -> None: + """Validate explicit total limits, ordering and mutually exclusive owner filters. + + None means a complete scan subject to explicit quota errors. Filters combine + with AND. Workers retain filters, order and the remaining total limit in + continuation state, bound to the authenticated tenant/caller. Ownership + scopes select among authorized rows, never grant access. Exact owner and + non-all scope are mutually exclusive. parent_owner ANDs with either and + matches only the recorded direct parent, never organizational descendants. + + Workers preserve admitted membership selection without materializing the + directory and recheck current membership and access on each continuation. + Membership revocations may remove pending rows; newly gained membership + cannot add rows to an admitted stream. If selection cannot be preserved, + fail explicitly; never restart with a widened scope. + """ + if limit is not None and (isinstance(limit, bool) or not isinstance(limit, int) or limit < 1): + raise ValueError("limit must be a positive integer or None") + if order_by not in supported_orders: + raise ValueError("Unsupported ordering") + if ownership_scope not in {"all", "self", "member", "self_or_member"}: + raise ValueError("Unknown ownership scope") + if ownership_scope != "all" and owner is not None: + raise ValueError("Exact owner and non-all ownership_scope are mutually exclusive") + for key in (owner, parent_owner): + if key is not None: + validate_owner_key(key) + if ownership_capabilities is not None: + validate_ownership_capabilities(ownership_capabilities) + if ownership_scope not in ownership_capabilities.scopes: + raise NotImplementedError("Ownership scope is unsupported") + if owner is not None and owner.kind not in ownership_capabilities.owner_kinds: + raise ValueError("Unsupported owner kind") + if parent_owner is not None and parent_owner.kind not in ownership_capabilities.parent_owner_kinds: + raise ValueError("Unsupported parent owner kind") + elif ownership_scope in {"member", "self_or_member"}: + raise NotImplementedError("Membership filtering requires advertised support") +``` diff --git a/docs/design/reporting-protocols/reference/notify.md b/docs/design/reporting-protocols/reference/notify.md new file mode 100644 index 00000000..8dc09630 --- /dev/null +++ b/docs/design/reporting-protocols/reference/notify.md @@ -0,0 +1,206 @@ +# Notifications: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/notify.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../notify.md) · [Shared records](common.md) + +Protocol `vgi.notify.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `check_destinations` | `destinations` | `DestinationCheck` | Producer batches | Function | +| `find_destinations` | `kind`, `query`, `limit` | `DestinationRecord` | Producer batches | Function | +| `get_notification` | `notification_id` | `NotificationReceipt` | `result: binary` (IPC record) | Function | +| `get_notify_info` | — | `NotifyInfo` | `result: binary` (IPC record) | Function; table `info` | +| `list_destinations` | `kind`, `limit` | `DestinationRecord` | Producer batches | Function | +| `list_notification_results` | `notification_id`, `limit` | `NotificationDelivery` | Producer batches | Function | +| `send` | `request_id`, `destinations`, `notification` | `NotificationReceipt` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.notify.v1 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Literal, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import Instant, non_null_list +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import ( + Destination, + Limit, + Notification, + RunError, + ServiceInfo, +) +from vgi.reporting.discovery import CheckState, DiscoveryCapabilities + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DestinationRecord(Destination): + """Discoverable destination metadata.""" + + display_name: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DestinationCheck(DestinationRecord): + """Independent advisory result correlated to its zero-based input index. + + Allowed means policy permits sending, not that a mailbox exists. Unknown + never counts as allowed. Every input gets exactly one row on successful EOS; + workers may emit completion order. Partial streams are incomplete checks. + """ + + input_index: int + state: CheckState + reason_code: str | None + reason: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ChannelInfo(ArrowSerializableDataclass): + """Worker-supported channel features and typed limits.""" + + kind: str + enabled: bool + images: bool + threads: bool + limits: Annotated[list[Limit], non_null_list(pa.struct(Limit.ARROW_SCHEMA))] + html_body: bool = False + discovery: DiscoveryCapabilities = field(default_factory=DiscoveryCapabilities) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class NotifyInfo(ServiceInfo): + """Supported channels and destination policy summary. + + Top-level limits require finite max_check_destinations and + max_send_destinations counts across all channels in a request. Channel limits + describe per-channel content restrictions, not separate batch allowances. + """ + + channels: Annotated[list[ChannelInfo], non_null_list(pa.struct(ChannelInfo.ARROW_SCHEMA))] + destination_policy_summary: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class NotificationReceipt(ArrowSerializableDataclass): + """Compact service admission and current counts; destination outcomes are streamed. + + send replays the original admission snapshot; get_notification returns current + progress. Counts partition all destinations. Provider acceptance, not service + admission, contributes to a schedule's last_delivery_at. Retention expiry + never authorizes resending a request with an unknown outcome. + """ + + notification_id: str + version: int + admitted_at: Instant + updated_at: Instant + retained_until: Instant + poll_after_seconds: int + queued_count: int + provider_accepted_count: int + refused_count: int + failed_count: int + unknown_count: int + last_provider_accepted_at: Instant | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class NotificationDelivery(DestinationRecord): + """Current outcome for one original destination, never proof of inbox delivery. + + Unknown requires provider reconciliation/deduplication before dispatch. + Confirmed destinations are never resent to recover another destination. + """ + + input_index: int + state: Annotated[Literal["queued", "provider_accepted", "refused", "failed", "unknown"], ArrowType(pa.string())] + provider_accepted_at: Instant | None + provider_reference: str | None + error: RunError | None + + +class NotifyProtocol(Protocol): + """Proposed vgi.notify.v1 records and RPC interface; implementations supply all methods.""" + + protocol_name: ClassVar[str] = "vgi.notify.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read(table="info") + def get_notify_info(self) -> NotifyInfo: + """Get notify info. + + Per kind: enabled, limits (text length, attachment bytes), images, threads; destination policy summary. + """ + ... + + @sql_read(row_type=DestinationRecord) + def list_destinations(self, kind: str, limit: int | None = None) -> Stream[ProducerState]: + """Optional enumeration for one channel; require enumeration_supported. + + Unsupported channels return UNIMPLEMENTED. This is not an automatic SQL + table. limit bounds the total rows, and continuations recheck authorization. + """ + ... + + @sql_read(row_type=DestinationRecord) + def find_destinations(self, kind: str, query: str, limit: int = 20) -> Stream[ProducerState]: + """Bounded search/suggestions under channel discovery policy, with no implicit enumeration. + + limit applies across every continuation. Reaching it invites refinement; + continuation means unfinished delivery, not additional directory matches. + """ + ... + + @sql_read(row_type=DestinationCheck) + def check_destinations( + self, destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))] + ) -> Stream[ProducerState]: + """Exact checks independent of discovery; enforce max_check_destinations before admission.""" + ... + + def send( + self, + request_id: str, + destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))], + notification: Notification, + ) -> NotificationReceipt: + """Durably admit a bounded send and return its original receipt on request replay. + + Enforce max_send_destinations and byte limits before admission. Store + each dispatch state before provider calls. Current authorization is + required for admission and replay; check_destinations is advisory only. + """ + ... + + @sql_read() + def get_notification(self, notification_id: str) -> NotificationReceipt: + """Current progress for the admitting caller or authorized administrator; NOT_FOUND after retention.""" + ... + + @sql_read(row_type=NotificationDelivery) + def list_notification_results(self, notification_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized snapshot of outcomes in original input-index order; no implicit resend.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/objects.md b/docs/design/reporting-protocols/reference/objects.md new file mode 100644 index 00000000..21c5296c --- /dev/null +++ b/docs/design/reporting-protocols/reference/objects.md @@ -0,0 +1,389 @@ +# Objects: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/objects.py`. Edit the Python module and regenerate this page. + +[Shared records](common.md) + +Protocol `vgi.objects.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `commit_revision` | `object_id`, `expected_revision_id`, `request_id`, `envelope`, `body`, `kind`, `message` | `ObjectResult` | `result: binary` (IPC record) | — | +| `create_folder` | `request_id`, `name`, `parent_folder_id`, `ownership` | `ObjectFolder` | `result: binary` (IPC record) | — | +| `create_object` | `request_id`, `object_kind`, `envelope`, `body`, `message`, `ownership`, `folder_id` | `ObjectResult` | `result: binary` (IPC record) | — | +| `delete_folder` | `folder_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `delete_object` | `object_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `get_folder` | `folder_id` | `ObjectFolder` | `result: binary` (IPC record) | Function | +| `get_object` | `object_id`, `revision_id`, `selection` | `ObjectResult` | `result: binary` (IPC record) | Function | +| `get_objects_info` | — | `ObjectsInfo` | `result: binary` (IPC record) | Function; table `info` | +| `list_folders` | `parent_folder_id`, `recursive`, `query`, `ownership_scope`, `owner`, `parent_owner`, `limit`, `order_by` | `ObjectFolder` | Producer batches | Function | +| `list_objects` | `query`, `folder_id`, `recursive`, `tags`, `ownership_scope`, `published_only`, `object_kind`, `owner`, `parent_owner`, `limit`, `order_by` | `ObjectSummary` | Producer batches | Function | +| `list_revisions` | `object_id`, `before_revision`, `limit` | `ObjectRevision` | Producer batches | Function | +| `move_object` | `object_id`, `expected_version`, `request_id`, `folder_id` | `ObjectRow` | `result: binary` (IPC record) | — | +| `publish` | `object_id`, `revision_id`, `expected_published_revision_id`, `request_id` | `ObjectResult` | `result: binary` (IPC record) | — | +| `redact_revision` | `object_id`, `revision_id`, `expected_version`, `request_id`, `reason` | `ObjectRevision` | `result: binary` (IPC record) | — | +| `scan_folders` | `parent_folder_id`, `recursive`, `query`, `ownership_scope`, `owner`, `parent_owner`, `limit`, `order_by` | `ObjectFolder` | Producer batches | Function; table `folders` | +| `scan_objects` | `query`, `folder_id`, `recursive`, `tags`, `ownership_scope`, `published_only`, `object_kind`, `owner`, `parent_owner`, `limit`, `order_by` | `ObjectRow` | Producer batches | Function; table `objects` | +| `set_folder_ownership` | `folder_id`, `expected_version`, `request_id`, `ownership` | `ObjectFolder` | `result: binary` (IPC record) | — | +| `set_ownership` | `object_id`, `expected_version`, `request_id`, `ownership` | `ObjectResult` | `result: binary` (IPC record) | — | +| `update_folder` | `folder_id`, `expected_version`, `request_id`, `name`, `parent_folder_id` | `ObjectFolder` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.objects.v1 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import Instant, non_null_list +from vgi.reporting._enums import RevisionKind +from vgi.reporting._metadata import FetchedColumn, sql_read +from vgi.reporting.common import ( + Ack, + DataSource, + OwnerKey, + Ownership, + OwnershipCapabilities, + OwnershipInput, + OwnershipScope, + ParameterSpec, + PrincipalRef, + ResourceMeta, + ServiceInfo, +) +from vgi.reporting.sources import RevisionSelection + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectEnvelope(ArrowSerializableDataclass): + """Public document metadata, source attachments and parameter declarations.""" + + title: str + description: str = "" + tags: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + body_format: str + data_sources: Annotated[list[DataSource], non_null_list(pa.struct(DataSource.ARROW_SCHEMA))] = field( + default_factory=list + ) + parameters: Annotated[list[ParameterSpec], non_null_list(pa.struct(ParameterSpec.ARROW_SCHEMA))] = field( + default_factory=list + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectRow(ResourceMeta): + """Visible object revision, current folder and management metadata, without body bytes.""" + + object_id: str + object_kind: str + folder_id: str | None + head_revision_id: str + published_revision_id: str | None + revision_served: str + revision_number: int + envelope: ObjectEnvelope | None + body_sha256: str | None + redacted: bool + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectSummary(ResourceMeta): + """Lightweight visible revision metadata without bindings, parameter options or body.""" + + object_id: str + object_kind: str + folder_id: str | None + title: str | None + body_format: str | None + revision_served: str + body_sha256: str | None + redacted: bool + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectResult(ObjectRow): + """One visible object revision, including its nullable redacted body.""" + + body: bytes | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectRevision(ArrowSerializableDataclass): + """Immutable revision identity plus publication and redaction metadata.""" + + object_id: str + revision_id: str + revision_number: int + author: PrincipalRef + created_at: Instant + kind: RevisionKind + message: str + envelope: ObjectEnvelope | None + body_sha256: str | None + published_at: Instant | None + published_by: PrincipalRef | None + redacted_at: Instant | None + redacted_by: PrincipalRef | None + redaction_reason: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectFolder(ResourceMeta): + """Persistent folder; a null parent selects root. + + Workers may set folder policies using their own authorization model. + Inherited allowed_actions reports caller-specific permission hints. + """ + + folder_id: str + parent_folder_id: str | None + name: str + creatable_kinds: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectKindInfo(ArrowSerializableDataclass): + """Advertised object kind and accepted versioned body formats.""" + + kind: str + body_formats: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectsInfo(ServiceInfo): + """Object store formats, limits and caller-specific root actions.""" + + ownership: OwnershipCapabilities + kinds: Annotated[list[ObjectKindInfo], non_null_list(pa.struct(ObjectKindInfo.ARROW_SCHEMA))] + creatable_kinds: Annotated[list[str], non_null_list(pa.string())] + root_allowed_actions: Annotated[list[str], non_null_list(pa.string())] + + +class ObjectsProtocol(Protocol): + """Proposed vgi.objects.v1 records and RPC interface; implementations supply all methods. + + Workers may define and enforce policies on folders and objects. The protocol + exposes permission hints and standard refusals; policy implementation and + inheritance are worker choices. + + OwnershipCapabilities defines all list/scan scope semantics. Exact owner + cannot be combined with a non-all ownership_scope. parent_owner matches + only the recorded direct management parent; folder recursion never expands + directory ancestry. Filters select authorized resources and never grant access. + """ + + protocol_name: ClassVar[str] = "vgi.objects.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read(table="info") + def get_objects_info(self) -> ObjectsInfo: + """display_name, writable, body formats, limits and allowed actions at root.""" + ... + + @sql_read( + row_type=ObjectSummary, + fetched_columns=( + FetchedColumn("body", "get_object", (("object_id", "object_id"), ("revision_id", "revision_served"))), + ), + ) + def list_objects( + self, + query: str = "", + folder_id: str | None = None, + recursive: bool = False, + tags: Annotated[list[str], non_null_list(pa.string())] | None = None, + ownership_scope: OwnershipScope = "all", + published_only: bool = False, + object_kind: str = "", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Browse visible summaries, direct children by default, with AND-combined filters. + + Order by id, title or updated_desc, breaking ties by object_id. + Owner keys are exact and a non-all ownership_scope cannot be combined with owner. limit bounds the + entire stream; snapshots and authorization checks survive continuation. + """ + ... + + @sql_read( + table="objects", + row_type=ObjectRow, + fetched_columns=( + FetchedColumn("body", "get_object", (("object_id", "object_id"), ("revision_id", "revision_served"))), + ), + ) + def scan_objects( + self, + query: str = "", + folder_id: str | None = None, + recursive: bool = True, + tags: Annotated[list[str], non_null_list(pa.string())] | None = None, + ownership_scope: OwnershipScope = "all", + published_only: bool = False, + object_kind: str = "", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Explicit full metadata export/SQL table, recursively by default; bodies remain fetched lazily.""" + ... + + @sql_read() + def get_object( + self, object_id: str, revision_id: str | None = None, selection: RevisionSelection | None = None + ) -> ObjectResult: + """Envelope and body. + + With neither selector, workers choose the authorized default view. revision_id is a + pinned shorthand used by SQL fetched columns; it cannot accompany selection. + Explicit head/published selections never silently fall back. revision_served identifies the result. + """ + ... + + @sql_read(row_type=ObjectRevision) + def list_revisions( + self, object_id: str, before_revision: int | None = None, limit: int | None = None + ) -> Stream[ProducerState]: + """Newest revision first, tombstones included; before_revision is exclusive, limit is total.""" + ... + + def create_object( + self, + request_id: str, + object_kind: str, + envelope: ObjectEnvelope, + body: bytes, + message: str = "", + ownership: OwnershipInput | None = None, + folder_id: str | None = None, + ) -> ObjectResult: + """New object with revision 1 in folder_id, or at root when None.""" + ... + + def move_object(self, object_id: str, expected_version: int, request_id: str, folder_id: str | None) -> ObjectRow: + """Move an object, with None selecting root; preserve object ID, revisions and publication.""" + ... + + def commit_revision( + self, + object_id: str, + expected_revision_id: str, + request_id: str, + envelope: ObjectEnvelope, + body: bytes, + kind: RevisionKind = "edit", + message: str = "", + ) -> ObjectResult: + """New head; conflict if the head moved.""" + ... + + def publish( + self, object_id: str, revision_id: str | None, expected_published_revision_id: str | None, request_id: str + ) -> ObjectResult: + """Move the published pointer; null revision_id unpublishes.""" + ... + + def delete_object(self, object_id: str, expected_version: int, request_id: str) -> Ack: + """Delete object.""" + ... + + def redact_revision( + self, object_id: str, revision_id: str, expected_version: int, request_id: str, reason: str + ) -> ObjectRevision: + """Requires redact.""" + ... + + def set_ownership( + self, object_id: str, expected_version: int, request_id: str, ownership: Ownership + ) -> ObjectResult: + """Transfer management ownership; preserve author and execution credentials.""" + ... + + @sql_read(row_type=ObjectFolder) + def list_folders( + self, + parent_folder_id: str | None = None, + recursive: bool = False, + query: str = "", + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Visible direct children by default; name substring query and id/name/updated_desc ordering. + + All filters combine with AND. Stable folder_id breaks ties; explicit + total limits and parent authorization apply across every continuation. + """ + ... + + @sql_read() + def get_folder(self, folder_id: str) -> ObjectFolder: + """Current folder metadata, ownership and caller-specific actions.""" + ... + + def create_folder( + self, + request_id: str, + name: str, + parent_folder_id: str | None = None, + ownership: OwnershipInput | None = None, + ) -> ObjectFolder: + """Create an empty folder under parent_folder_id, or at root when None.""" + ... + + def update_folder( + self, folder_id: str, expected_version: int, request_id: str, name: str, parent_folder_id: str | None + ) -> ObjectFolder: + """Atomically rename and/or move a folder; both destination values are required, cycles forbidden.""" + ... + + def delete_folder(self, folder_id: str, expected_version: int, request_id: str) -> Ack: + """Delete only an empty folder, checking all children atomically, including those hidden from the caller.""" + ... + + def set_folder_ownership( + self, folder_id: str, expected_version: int, request_id: str, ownership: Ownership + ) -> ObjectFolder: + """Transfer folder management; preserve descendants, their explicit ownership and all object content.""" + ... + + @sql_read(row_type=ObjectFolder, table="folders") + def scan_folders( + self, + parent_folder_id: str | None = None, + recursive: bool = True, + query: str = "", + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Full-tree SQL/export scan using the same filters and ordering as list_folders.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/ownership.md b/docs/design/reporting-protocols/reference/ownership.md new file mode 100644 index 00000000..8d245a1a --- /dev/null +++ b/docs/design/reporting-protocols/reference/ownership.md @@ -0,0 +1,88 @@ +# Report ownership discovery (optional): Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/ownership.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../reports.md) · [Shared records](common.md) + +Protocol `vgi.reports.ownership.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `find_owners` | `resource_kind`, `resource_id`, `query`, `limit` | `OwnershipCandidate` | Producer batches | Function | +| `get_ownership_capabilities` | `resource_kind`, `resource_id` | `OwnerDiscoveryCapabilities` | `result: binary` (IPC record) | Function | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Optional worker-controlled owner discovery for report and folder transfers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Annotated, ClassVar, Literal, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import Ownership +from vgi.reporting.discovery import OwnerDiscoveryCapabilities + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnershipCandidate(ArrowSerializableDataclass): + """A complete ownership assignment accepted as a candidate by this worker. + + Clients display the label and description and submit ownership unchanged. + Canonical identities, owner kinds and parent authorities are worker-owned. + """ + + label: str + description: str + ownership: Ownership + + +class ReportOwnershipProtocol(Protocol): + """Optional companion to ReportsProtocol, advertised through hosted protocols. + + Workers authorize discovery against the resource and control whether searches + are partial, exact identifier lookups, email resolution, or another scheme. + Results disclose only candidates available to this caller for this resource. + A candidate is not a grant: set_ownership/set_folder_ownership must revalidate + the complete assignment and caller at mutation time, with expected_version. + The parent authority may remain unchanged or be supplied by the worker. + """ + + protocol_name: ClassVar[str] = "vgi.reports.ownership.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read() + def get_ownership_capabilities(self, resource_kind: str, resource_id: str) -> OwnerDiscoveryCapabilities: + """Authorize the resource and describe accepted search input and limits.""" + ... + + @sql_read(row_type=OwnershipCandidate) + def find_owners( + self, + resource_kind: Annotated[Literal["report", "folder"], ArrowType(pa.string())], + resource_id: str, + query: str = "", + limit: int = 20, + ) -> Stream[ProducerState]: + """Stream at most limit candidates across all continuations. + + Search policy and query hints come from get_ownership_capabilities. + Empty queries require suggestion support. Exact-only clients use the + resource identity companion. Reaching the limit invites query refinement. + """ + ... +``` diff --git a/docs/design/reporting-protocols/reference/render.md b/docs/design/reporting-protocols/reference/render.md new file mode 100644 index 00000000..670b2fd1 --- /dev/null +++ b/docs/design/reporting-protocols/reference/render.md @@ -0,0 +1,156 @@ +# Rendering: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/render.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../render.md) · [Shared records](common.md) + +Protocol `vgi.report_render.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `cancel_render` | `job_id`, `expected_version`, `request_id` | `RenderJob` | `result: binary` (IPC record) | — | +| `check_sources` | `sources` | `SourceCheck` | Producer batches | Function | +| `get_render` | `job_id` | `RenderJob` | `result: binary` (IPC record) | Function | +| `get_renderer_info` | — | `RendererInfo` | `result: binary` (IPC record) | Function; table `info` | +| `start_render` | `request_id`, `request` | `RenderJob` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.report_render.v1 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Literal, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import Instant, non_null_list +from vgi.reporting._enums import RenderStatus +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import ( + Artifact, + DataSource, + Delegation, + ParamValue, + RunError, + ServiceInfo, +) +from vgi.reporting.discovery import CheckState + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RenderRequest(ArrowSerializableDataclass): + """Inline report body, job-scoped catalog credentials and output preferences.""" + + body_format: str + body: bytes + data_sources: Annotated[list[DataSource], non_null_list(pa.struct(DataSource.ARROW_SCHEMA))] + delegations: Annotated[list[Delegation], non_null_list(pa.struct(Delegation.ARROW_SCHEMA))] = field( + default_factory=list + ) + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] = field( + default_factory=list + ) + outputs: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=lambda: ["application/pdf"]) + locale: str = "en-US" + time_zone: str = "UTC" + timeout_seconds: int = 600 + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RenderJob(ArrowSerializableDataclass): + """Versioned render progress and downloadable artifacts.""" + + job_id: str + version: int + status: RenderStatus + created_at: Instant + started_at: Instant | None + finished_at: Instant | None + poll_after_seconds: int + artifacts: Annotated[list[Artifact], non_null_list(pa.struct(Artifact.ARROW_SCHEMA))] + error: RunError | None + allowed_actions: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RendererInfo(ServiceInfo): + """Formats and source policy; explicit_list requires a list, dynamic requires None. + + An explicit empty list permits no locations. None means the policy cannot be + enumerated; check_sources remains available. Limits include max_source_checks. + """ + + body_formats: Annotated[list[str], non_null_list(pa.string())] + output_media_types: Annotated[list[str], non_null_list(pa.string())] + source_policy: Annotated[Literal["explicit_list", "dynamic"], ArrowType(pa.string())] + allowed_source_locations: Annotated[list[str], non_null_list(pa.string())] | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SourceCheck(ArrowSerializableDataclass): + """Renderer outbound-policy decision, not a credential or query authorization. + + Exactly one row per input_index on successful EOS, in any completion order. + Unknown/unfinished checks cannot be treated as permission. Dynamic policy + need not disclose the complete permitted set of locations. + """ + + input_index: int + source: DataSource + state: CheckState + reason_code: str | None + message: str | None + + +class ReportRenderProtocol(Protocol): + """Proposed vgi.report_render.v1 records and RPC interface; implementations supply all methods.""" + + protocol_name: ClassVar[str] = "vgi.report_render.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read(table="info") + def get_renderer_info(self) -> RendererInfo: + """Body formats, output media types, timeout ceiling, allowed source locations.""" + ... + + @sql_read(row_type=SourceCheck) + def check_sources( + self, sources: Annotated[list[DataSource], non_null_list(pa.struct(DataSource.ARROW_SCHEMA))] + ) -> Stream[ProducerState]: + """Check supplied references without user SQL; enforce max_source_checks and recheck at execution.""" + ... + + def start_render(self, request_id: str, request: RenderRequest) -> RenderJob: + """Returns job_id, status queued.""" + ... + + @sql_read() + def get_render(self, job_id: str) -> RenderJob: + """Get render. + + Status (queued, running, succeeded, failed, cancelled), poll_after_seconds, artifacts, and on failure the + cause's code, kind and data source. + """ + ... + + def cancel_render(self, job_id: str, expected_version: int, request_id: str) -> RenderJob: + """Best effort.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/reports.md b/docs/design/reporting-protocols/reference/reports.md new file mode 100644 index 00000000..8da90f56 --- /dev/null +++ b/docs/design/reporting-protocols/reference/reports.md @@ -0,0 +1,370 @@ +# Reports: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/reports.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../reports.md) · [Shared records](common.md) + +Protocol `vgi.reports.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `commit_revision` | `report_id`, `expected_revision_id`, `request_id`, `envelope`, `body`, `kind`, `message` | `ReportResult` | `result: binary` (IPC record) | — | +| `create_folder` | `request_id`, `name`, `parent_folder_id`, `ownership` | `FolderRecord` | `result: binary` (IPC record) | — | +| `create_report` | `request_id`, `envelope`, `body`, `message`, `ownership`, `folder_id` | `ReportResult` | `result: binary` (IPC record) | — | +| `delete_folder` | `folder_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `delete_report` | `report_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `get_folder` | `folder_id` | `FolderRecord` | `result: binary` (IPC record) | Function | +| `get_report` | `report_id`, `revision_id` | `ReportResult` | `result: binary` (IPC record) | Function | +| `get_report_service_info` | — | `ReportsInfo` | `result: binary` (IPC record) | Function; table `info` | +| `list_folders` | `parent_folder_id`, `recursive`, `query`, `ownership_scope`, `owner`, `parent_owner`, `limit`, `order_by` | `FolderRecord` | Producer batches | Function | +| `list_reports` | `query`, `folder_id`, `recursive`, `tags`, `ownership_scope`, `published_only`, `owner`, `parent_owner`, `limit`, `order_by` | `ReportSummary` | Producer batches | Function | +| `list_revisions` | `report_id`, `before_revision`, `limit` | `RevisionRow` | Producer batches | Function | +| `move_report` | `report_id`, `expected_version`, `request_id`, `folder_id` | `ReportRow` | `result: binary` (IPC record) | — | +| `publish` | `report_id`, `revision_id`, `expected_published_revision_id`, `request_id` | `ReportResult` | `result: binary` (IPC record) | — | +| `redact_revision` | `report_id`, `revision_id`, `expected_version`, `request_id`, `reason` | `RevisionRow` | `result: binary` (IPC record) | — | +| `scan_folders` | `parent_folder_id`, `recursive`, `query`, `ownership_scope`, `owner`, `parent_owner`, `limit`, `order_by` | `FolderRecord` | Producer batches | Function; table `folders` | +| `scan_reports` | `query`, `folder_id`, `recursive`, `tags`, `ownership_scope`, `published_only`, `owner`, `parent_owner`, `limit`, `order_by` | `ReportRow` | Producer batches | Function; table `reports` | +| `set_folder_ownership` | `folder_id`, `expected_version`, `request_id`, `ownership` | `FolderRecord` | `result: binary` (IPC record) | — | +| `set_ownership` | `report_id`, `expected_version`, `request_id`, `ownership` | `ReportResult` | `result: binary` (IPC record) | — | +| `update_folder` | `folder_id`, `expected_version`, `request_id`, `name`, `parent_folder_id` | `FolderRecord` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.reports.v1 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import Instant, non_null_list +from vgi.reporting._enums import RevisionKind +from vgi.reporting._metadata import FetchedColumn, sql_read +from vgi.reporting.common import ( + Ack, + DataSource, + OwnerKey, + Ownership, + OwnershipCapabilities, + OwnershipInput, + OwnershipScope, + ParameterSpec, + PrincipalRef, + ResourceMeta, + ServiceInfo, +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ReportEnvelope(ArrowSerializableDataclass): + """Public report metadata, source attachments and parameter declarations.""" + + title: str + description: str = "" + tags: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + body_format: str + data_sources: Annotated[list[DataSource], non_null_list(pa.struct(DataSource.ARROW_SCHEMA))] = field( + default_factory=list + ) + parameters: Annotated[list[ParameterSpec], non_null_list(pa.struct(ParameterSpec.ARROW_SCHEMA))] = field( + default_factory=list + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ReportRow(ResourceMeta): + """Visible report revision, current folder and management metadata, without body bytes.""" + + report_id: str + folder_id: str | None + head_revision_id: str + published_revision_id: str | None + revision_served: str + revision_number: int + envelope: ReportEnvelope | None + body_sha256: str | None + redacted: bool + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ReportSummary(ResourceMeta): + """Lightweight visible revision metadata without bindings, parameter options or body.""" + + report_id: str + folder_id: str | None + title: str | None + body_format: str | None + revision_served: str + body_sha256: str | None + redacted: bool + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ReportResult(ReportRow): + """One visible report revision, including its nullable redacted body.""" + + body: bytes | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RevisionRow(ArrowSerializableDataclass): + """Immutable revision identity plus publication and redaction metadata.""" + + report_id: str + revision_id: str + revision_number: int + author: PrincipalRef + created_at: Instant + kind: RevisionKind + message: str + envelope: ReportEnvelope | None + body_sha256: str | None + published_at: Instant | None + published_by: PrincipalRef | None + redacted_at: Instant | None + redacted_by: PrincipalRef | None + redaction_reason: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class FolderRecord(ResourceMeta): + """Persistent folder; a null parent selects root. + + Workers may set folder policies using their own authorization model. + Inherited allowed_actions reports caller-specific permission hints. + """ + + folder_id: str + parent_folder_id: str | None + name: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ReportsInfo(ServiceInfo): + """Report store formats, limits and caller-specific root actions.""" + + ownership: OwnershipCapabilities + body_formats: Annotated[list[str], non_null_list(pa.string())] + root_allowed_actions: Annotated[list[str], non_null_list(pa.string())] + + +class ReportsProtocol(Protocol): + """Proposed vgi.reports.v1 records and RPC interface; implementations supply all methods. + + Workers may define and enforce policies on folders and reports. The protocol + exposes permission hints and standard refusals; policy implementation and + inheritance are worker choices. + + OwnershipCapabilities defines all list/scan scope semantics. Exact owner + cannot be combined with a non-all ownership_scope. parent_owner matches + only the recorded direct management parent; folder recursion never expands + directory ancestry. Filters select authorized resources and never grant access. + """ + + protocol_name: ClassVar[str] = "vgi.reports.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read(table="info") + def get_report_service_info(self) -> ReportsInfo: + """display_name, writable, body formats, limits and allowed actions at root.""" + ... + + @sql_read( + row_type=ReportSummary, + fetched_columns=( + FetchedColumn("body", "get_report", (("report_id", "report_id"), ("revision_id", "revision_served"))), + ), + ) + def list_reports( + self, + query: str = "", + folder_id: str | None = None, + recursive: bool = False, + tags: Annotated[list[str], non_null_list(pa.string())] | None = None, + ownership_scope: OwnershipScope = "all", + published_only: bool = False, + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Browse visible summaries, direct children by default, with AND-combined filters. + + Order by id, title or updated_desc, breaking ties by report_id. + Owner keys are exact and a non-all ownership_scope cannot be combined with owner. limit bounds the + entire stream; snapshots and authorization checks survive continuation. + """ + ... + + @sql_read( + table="reports", + row_type=ReportRow, + fetched_columns=( + FetchedColumn("body", "get_report", (("report_id", "report_id"), ("revision_id", "revision_served"))), + ), + ) + def scan_reports( + self, + query: str = "", + folder_id: str | None = None, + recursive: bool = True, + tags: Annotated[list[str], non_null_list(pa.string())] | None = None, + ownership_scope: OwnershipScope = "all", + published_only: bool = False, + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Explicit full metadata export/SQL table, recursively by default; bodies remain fetched lazily.""" + ... + + @sql_read() + def get_report(self, report_id: str, revision_id: str | None = None) -> ReportResult: + """Envelope and body. + + With no revision id, editors get the head and everyone else the published revision; revision_served says + which. + """ + ... + + @sql_read(row_type=RevisionRow) + def list_revisions( + self, report_id: str, before_revision: int | None = None, limit: int | None = None + ) -> Stream[ProducerState]: + """Newest revision first, tombstones included; before_revision is exclusive, limit is total.""" + ... + + def create_report( + self, + request_id: str, + envelope: ReportEnvelope, + body: bytes, + message: str = "", + ownership: OwnershipInput | None = None, + folder_id: str | None = None, + ) -> ReportResult: + """New report with revision 1 in folder_id, or at root when None.""" + ... + + def move_report(self, report_id: str, expected_version: int, request_id: str, folder_id: str | None) -> ReportRow: + """Move a report, with None selecting root; preserve report ID, revisions and publication.""" + ... + + def commit_revision( + self, + report_id: str, + expected_revision_id: str, + request_id: str, + envelope: ReportEnvelope, + body: bytes, + kind: RevisionKind = "edit", + message: str = "", + ) -> ReportResult: + """New head; conflict if the head moved.""" + ... + + def publish( + self, report_id: str, revision_id: str | None, expected_published_revision_id: str | None, request_id: str + ) -> ReportResult: + """Move the published pointer; null revision_id unpublishes.""" + ... + + def delete_report(self, report_id: str, expected_version: int, request_id: str) -> Ack: + """Delete report.""" + ... + + def redact_revision( + self, report_id: str, revision_id: str, expected_version: int, request_id: str, reason: str + ) -> RevisionRow: + """Requires redact.""" + ... + + def set_ownership( + self, report_id: str, expected_version: int, request_id: str, ownership: Ownership + ) -> ReportResult: + """Transfer management ownership; preserve author and execution credentials.""" + ... + + @sql_read(row_type=FolderRecord) + def list_folders( + self, + parent_folder_id: str | None = None, + recursive: bool = False, + query: str = "", + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Visible direct children by default; name substring query and id/name/updated_desc ordering. + + All filters combine with AND. Stable folder_id breaks ties; explicit + total limits and parent authorization apply across every continuation. + """ + ... + + @sql_read() + def get_folder(self, folder_id: str) -> FolderRecord: + """Current folder metadata, ownership and caller-specific actions.""" + ... + + def create_folder( + self, + request_id: str, + name: str, + parent_folder_id: str | None = None, + ownership: OwnershipInput | None = None, + ) -> FolderRecord: + """Create an empty folder under parent_folder_id, or at root when None.""" + ... + + def update_folder( + self, folder_id: str, expected_version: int, request_id: str, name: str, parent_folder_id: str | None + ) -> FolderRecord: + """Atomically rename and/or move a folder; both destination values are required, cycles forbidden.""" + ... + + def delete_folder(self, folder_id: str, expected_version: int, request_id: str) -> Ack: + """Delete only an empty folder, checking all children atomically, including those hidden from the caller.""" + ... + + def set_folder_ownership( + self, folder_id: str, expected_version: int, request_id: str, ownership: Ownership + ) -> FolderRecord: + """Transfer folder management; preserve descendants, their explicit ownership and all report content.""" + ... + + @sql_read(row_type=FolderRecord, table="folders") + def scan_folders( + self, + parent_folder_id: str | None = None, + recursive: bool = True, + query: str = "", + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Full-tree SQL/export scan using the same filters and ordering as list_folders.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/resource_identity.md b/docs/design/reporting-protocols/reference/resource_identity.md new file mode 100644 index 00000000..398c020b --- /dev/null +++ b/docs/design/reporting-protocols/reference/resource_identity.md @@ -0,0 +1,212 @@ +# Resource identity checks: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/resource_identity.py`. Edit the Python module and regenerate this page. + +[Shared records](common.md) + +Protocol `vgi.resource_identity.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `check_execution_principal` | `context`, `principal` | `ExecutionPrincipalCheck` | `result: binary` (IPC record) | Function | +| `check_ownership` | `context`, `owner`, `parent` | `OwnershipCheck` | `result: binary` (IPC record) | Function | +| `find_execution_principals` | `context`, `query`, `limit` | `ExecutionCandidate` | Producer batches | Function | +| `find_owners` | `context`, `query`, `limit` | `OwnershipCandidate` | Producer batches | Function | +| `get_identity_capabilities` | `context` | `IdentityCapabilities` | `result: binary` (IPC record) | Function | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Resource-scoped identity resolution without a required directory listing. + +Exact checks are required for each advertised purpose; searches are optional. +Workers authorize the context before contacting a directory and revalidate at +mutation time. Missing and hidden identities may share a denial, but directory +outages must yield unknown or UNAVAILABLE, never a successful empty lookup. +""" + +from dataclasses import dataclass +from typing import Annotated, ClassVar, Literal, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import Ownership, PrincipalRef +from vgi.reporting.discovery import ( + CheckState, + DiscoveryCapabilities, + OwnerDiscoveryCapabilities, + validate_ownership_capabilities, +) +from vgi.reporting.ownership import OwnershipCandidate + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ResourceContext(ArrowSerializableDataclass): + """Creation scope or an existing resource whose identity may be changed. + + Creation has no resource_id. Changes require one. parent_folder_id selects + an authorized creation folder, otherwise root; it is forbidden on changes + and for resource kinds without folders. Null object IDs are never wildcards. + The host binds this context to the authenticated tenant before resolution; + IDs, parent selections and identifier schemes cannot change that tenant. + """ + + protocol: str + resource_kind: str + operation: Annotated[Literal["create", "transfer_ownership", "change_execution"], ArrowType(pa.string())] + resource_id: str | None = None + parent_folder_id: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Identifier(ArrowSerializableDataclass): + """Exact advertised scheme/value resolved only within the authorized tenant. + + Schemes must distinguish principal, group/team and organizational identifiers + when their provider-local values can collide. Ambiguous input is refused; + email domains, labels and client-supplied issuer hints never select a tenant. + """ + + scheme: str + value: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class IdentityCapabilities(ArrowSerializableDataclass): + """Per-context purposes; None means that identity purpose is unsupported.""" + + ownership: OwnerDiscoveryCapabilities | None + execution: DiscoveryCapabilities | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnershipCheck(ArrowSerializableDataclass): + """Allowed checks return complete canonical ownership; others return None.""" + + state: CheckState + ownership: Ownership | None + reason_code: str | None + message: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ExecutionCandidate(ArrowSerializableDataclass): + """Eligible canonical principal; workload credential readiness is checked separately.""" + + principal: PrincipalRef + description: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ExecutionPrincipalCheck(ArrowSerializableDataclass): + """Allowed checks return the canonical principal, without granting impersonation.""" + + state: CheckState + principal: PrincipalRef | None + reason_code: str | None + message: str | None + + +def validate_resource_context(context: ResourceContext, *, supports_folders: bool) -> None: + """Reject ambiguous creation/change contexts before resolving identity.""" + if not context.protocol or not context.resource_kind: + raise ValueError("Context requires protocol and resource kind") + if context.operation == "create": + if context.resource_id is not None: + raise ValueError("Creation cannot name an existing resource") + elif context.operation in {"transfer_ownership", "change_execution"}: + if not context.resource_id or context.parent_folder_id is not None: + raise ValueError("Changes require a resource ID and use its existing context") + else: + raise ValueError("Unsupported identity operation") + if context.parent_folder_id is not None and (not supports_folders or not context.parent_folder_id): + raise ValueError("A nonempty folder ID requires folder support") + + +def validate_identity_request( + context: ResourceContext, + *, + purpose: Literal["ownership", "execution"], + identifier: Identifier | None, + capabilities: IdentityCapabilities, + supports_folders: bool, +) -> None: + """Validate exact lookup shape, purpose and advertised schemes before contacting a directory. + + Workers must separately authorize the context, resolve canonical identities, + check consent/eligibility, and repeat those checks at mutation time. + """ + validate_resource_context(context, supports_folders=supports_folders) + if purpose not in {"ownership", "execution"}: + raise ValueError("Unknown identity purpose") + discovery = capabilities.ownership if purpose == "ownership" else capabilities.execution + if discovery is None: + raise NotImplementedError("Identity purpose is unsupported") + if purpose == "ownership" and capabilities.ownership is not None: + validate_ownership_capabilities(capabilities.ownership.policy) + operation = "transfer_ownership" if purpose == "ownership" else "change_execution" + if context.operation not in {"create", operation}: + raise ValueError("Context operation does not match the identity purpose") + if identifier is None: + if context.operation != "create": + raise ValueError("Identity changes require an explicit identifier") + elif identifier.scheme not in discovery.identifier_schemes or not identifier.value.strip(): + raise ValueError("Supply a nonempty identifier using an advertised scheme") + + +class ResourceIdentityProtocol(Protocol): + """Optional companion for canonical identity selection within authorized resources.""" + + protocol_name: ClassVar[str] = "vgi.resource_identity.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read() + def get_identity_capabilities(self, context: ResourceContext) -> IdentityCapabilities: + """Authorize context and advertise exact schemes and optional search per purpose.""" + ... + + @sql_read() + def check_ownership( + self, context: ResourceContext, owner: Identifier | None = None, parent: Identifier | None = None + ) -> OwnershipCheck: + """Resolve a complete assignment; null requests the worker's default/safe parent. + + Valid only for create/transfer_ownership. Transfers require an explicit + owner; null parent allows the worker to preserve or resolve a safe parent. + Authorize owner eligibility and the direct durable-parent relationship + separately; membership alone does not permit assigning either authority. + """ + ... + + @sql_read(row_type=OwnershipCandidate) + def find_owners(self, context: ResourceContext, query: str, limit: int = 20) -> Stream[ProducerState]: + """Bounded candidates for create/transfer_ownership; use advertised search policy.""" + ... + + @sql_read() + def check_execution_principal( + self, context: ResourceContext, principal: Identifier | None = None + ) -> ExecutionPrincipalCheck: + """Check selection eligibility for create/change_execution, including consent. + + Null selects the caller only for creation. Changes require an explicit + identifier. Readiness for particular sources belongs to workload validation. + """ + ... + + @sql_read(row_type=ExecutionCandidate) + def find_execution_principals(self, context: ResourceContext, query: str, limit: int = 20) -> Stream[ProducerState]: + """Bounded eligible principals for create/change_execution; enumeration is unnecessary.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/resource_ownership.md b/docs/design/reporting-protocols/reference/resource_ownership.md new file mode 100644 index 00000000..9428f72a --- /dev/null +++ b/docs/design/reporting-protocols/reference/resource_ownership.md @@ -0,0 +1,80 @@ +# Resource ownership discovery: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/resource_ownership.py`. Edit the Python module and regenerate this page. + +[Shared records](common.md) + +Protocol `vgi.ownership.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `find_owners` | `protocol`, `resource_kind`, `resource_id`, `query`, `limit` | `OwnershipCandidate` | Producer batches | Function | +| `get_ownership_capabilities` | `protocol`, `resource_kind`, `resource_id` | `OwnerDiscoveryCapabilities` | `result: binary` (IPC record) | Function | +| `get_ownership_info` | — | `OwnershipInfo` | `result: binary` (IPC record) | Function | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Optional worker-controlled ownership discovery for advertised resources.""" + +from dataclasses import dataclass +from typing import Annotated, ClassVar, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import non_null_list +from vgi.reporting._metadata import sql_read +from vgi.reporting.discovery import OwnerDiscoveryCapabilities +from vgi.reporting.ownership import OwnershipCandidate + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnershipResource(ArrowSerializableDataclass): + """A locally hosted protocol and supported ownership resource kind.""" + + protocol: str + resource_kind: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OwnershipInfo(ArrowSerializableDataclass): + """Resource types supported by this worker's ownership directory.""" + + resources: Annotated[list[OwnershipResource], non_null_list(pa.struct(OwnershipResource.ARROW_SCHEMA))] + + +class OwnershipProtocol(Protocol): + """Discovery is advisory; mutations revalidate identity, permissions and version.""" + + protocol_name: ClassVar[str] = "vgi.ownership.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read() + def get_ownership_info(self) -> OwnershipInfo: + """Advertise supported local protocol/resource pairs.""" + ... + + @sql_read() + def get_ownership_capabilities( + self, protocol: str, resource_kind: str, resource_id: str + ) -> OwnerDiscoveryCapabilities: + """Authorize the resource and return its search policy and query hint.""" + ... + + @sql_read(row_type=OwnershipCandidate) + def find_owners( + self, protocol: str, resource_kind: str, resource_id: str, query: str = "", limit: int = 20 + ) -> Stream[ProducerState]: + """Stream at most limit eligible owners across all continuations; obey advertised search policy.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/schedules.md b/docs/design/reporting-protocols/reference/schedules.md new file mode 100644 index 00000000..af02d0ba --- /dev/null +++ b/docs/design/reporting-protocols/reference/schedules.md @@ -0,0 +1,446 @@ +# Schedules: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/schedules.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../schedules.md) · [Shared records](common.md) + +Protocol `vgi.schedules.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `cancel_run` | `run_id`, `expected_version`, `request_id` | `ScheduleRun` | `result: binary` (IPC record) | — | +| `cancel_test_run` | `job_id`, `expected_version`, `request_id` | `SchedulePreviewJob` | `result: binary` (IPC record) | — | +| `create_schedule` | `request_id`, `schedule`, `ownership`, `execution` | `ScheduleRecord` | `result: binary` (IPC record) | — | +| `delete_schedule` | `schedule_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `get_run` | `run_id` | `ScheduleRun` | `result: binary` (IPC record) | Function | +| `get_schedule` | `schedule_id` | `ScheduleRecord` | `result: binary` (IPC record) | Function | +| `get_scheduler_info` | — | `SchedulerInfo` | `result: binary` (IPC record) | Function; table `info` | +| `get_test_run` | `job_id` | `SchedulePreviewJob` | `result: binary` (IPC record) | Function | +| `list_recovery_events` | `run_id`, `limit` | `RecoveryEvent` | Producer batches | Function | +| `list_run_destinations` | `run_id`, `limit` | `DestinationResult` | Producer batches | Function | +| `list_run_steps` | `run_id`, `limit` | `RunStep` | Producer batches | Function | +| `list_runs` | `schedule_id`, `status`, `since`, `before`, `execution_principal_id`, `limit` | `ScheduleRunSummary` | Producer batches | Function; table `runs` | +| `list_schedules` | `action_kind`, `report_id`, `ownership_scope`, `owner`, `parent_owner`, `execution_principal_id`, `query`, `enabled`, `issue_kind`, `limit`, `order_by` | `ScheduleSummary` | Producer batches | Function; table `schedules` | +| `list_step_attempts` | `run_id`, `step_index`, `limit` | `StepAttempt` | Producer batches | Function | +| `preview_trigger` | `trigger`, `after`, `count` | `TriggerPreview` | `result: binary` (IPC record) | Function | +| `resolve_run` | `run_id`, `expected_version`, `request_id`, `resolutions`, `note` | `ScheduleRun` | `result: binary` (IPC record) | — | +| `retry_run` | `run_id`, `expected_version`, `request_id` | `ScheduleRun` | `result: binary` (IPC record) | — | +| `run_now` | `schedule_id`, `request_id`, `ignore_condition` | `ScheduleRun` | `result: binary` (IPC record) | — | +| `scan_schedules` | `action_kind`, `report_id`, `ownership_scope`, `owner`, `parent_owner`, `execution_principal_id`, `query`, `enabled`, `issue_kind`, `limit`, `order_by` | `ScheduleRecord` | Producer batches | Function | +| `set_execution_principal` | `schedule_id`, `expected_version`, `request_id`, `principal_id` | `ScheduleRecord` | `result: binary` (IPC record) | — | +| `set_ownership` | `schedule_id`, `expected_version`, `request_id`, `ownership` | `ScheduleRecord` | `result: binary` (IPC record) | — | +| `start_test_run` | `request_id`, `schedule`, `execution`, `as_of` | `SchedulePreviewJob` | `result: binary` (IPC record) | — | +| `update_schedule` | `schedule_id`, `expected_version`, `request_id`, `schedule` | `ScheduleRecord` | `result: binary` (IPC record) | — | +| `validate_schedule` | `schedule`, `execution` | `ValidationResult` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.schedules.v1 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import Instant, Json, non_null_list +from vgi.reporting._enums import InlineMode, OutputFormat, ReportTracking +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import ( + Ack, + CredentialStatus, + DataSource, + Destination, + DestinationResult, + ExecutionIdentity, + ExecutionInput, + Notification, + Output, + OwnerKey, + Ownership, + OwnershipCapabilities, + OwnershipInput, + OwnershipScope, + ParamValue, + PreviewJob, + RecoveryEvent, + ReportRef, + ResourceMeta, + RetryPolicy, + Run, + RunStep, + RunSummary, + ServiceInfo, + StepAttempt, + StepResolution, + Trigger, + TriggerPreview, + ValidationResult, +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleRunSummary(RunSummary): + """Run status associated with its schedule, without the frozen definition.""" + + schedule_id: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleRun(Run): + """Schedule run with its frozen definition and selected report revision.""" + + schedule_id: str + schedule: Schedule + revision_id: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RenderReportAction(ArrowSerializableDataclass): + """Pinned or tracked report rendering action.""" + + report: ReportRef + track: ReportTracking = "published" + outputs: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=lambda: ["application/pdf"]) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RunQueryAction(ArrowSerializableDataclass): + """SQL setup/query action and requested output format.""" + + setup_sql: str = "" + sql: str + output_format: OutputFormat = "parquet" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Action(ArrowSerializableDataclass): + """Tagged schedule action; only the selected payload is populated.""" + + kind: str + render_report: RenderReportAction | None = None + run_query: RunQueryAction | None = None + data_sources: Annotated[list[DataSource], non_null_list(pa.struct(DataSource.ARROW_SCHEMA))] = field( + default_factory=list + ) + custom_json: Json | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Delivery(ArrowSerializableDataclass): + """Destinations and presentation preferences for one action delivery.""" + + destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))] + inline: InlineMode = "summary" + attach: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleAlerts(ArrowSerializableDataclass): + """Separate, worker-validated recipients for advertised schedule alert events. + + The worker owns warning thresholds, incident grouping and delivery policy. + Report delivery recipients are never implicitly subscribed to alerts. + """ + + destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))] + events: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleIssue(ArrowSerializableDataclass): + """Current worker-reported attention item, without credentials or raw errors.""" + + kind: str + message: str + run_id: str | None = None + expires_at: Instant | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Schedule(ArrowSerializableDataclass): + """Editable schedule definition; ownership and execution identity are separate.""" + + title: str + trigger: Trigger + action: Action + condition_sql: str = "" + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] = field( + default_factory=list + ) + deliveries: Annotated[list[Delivery], non_null_list(pa.struct(Delivery.ARROW_SCHEMA))] = field(default_factory=list) + enabled: bool = True + alerts: ScheduleAlerts | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleRecord(ResourceMeta): + """Persistent schedule definition, management identity and current status.""" + + schedule_id: str + definition: Schedule + execution_identity: ExecutionIdentity + next_fire_at: Instant | None + last_run: ScheduleRunSummary | None + disabled_reason: str | None + credentials: Annotated[list[CredentialStatus], non_null_list(pa.struct(CredentialStatus.ARROW_SCHEMA))] + issues: Annotated[list[ScheduleIssue], non_null_list(pa.struct(ScheduleIssue.ARROW_SCHEMA))] = field( + default_factory=list + ) + last_successful_at: Instant | None = None + last_delivery_at: Instant | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleSummary(ResourceMeta): + """Browseable schedule status without SQL, recipients, credentials or histories.""" + + schedule_id: str + title: str + action_kind: str + enabled: bool + execution_identity: ExecutionIdentity + next_fire_at: Instant | None + last_run: ScheduleRunSummary | None + disabled_reason: str | None + issues: Annotated[list[ScheduleIssue], non_null_list(pa.struct(ScheduleIssue.ARROW_SCHEMA))] + last_successful_at: Instant | None + last_delivery_at: Instant | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleTest(ArrowSerializableDataclass): + """Safe preview results and prospective messages without delivery.""" + + evaluated_at: Instant + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] + condition_value: bool | None + condition_evaluated: bool + outputs: Annotated[list[Output], non_null_list(pa.struct(Output.ARROW_SCHEMA))] + messages: Annotated[list[Notification], non_null_list(pa.struct(Notification.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SchedulePreviewJob(PreviewJob): + """Typed preview result, populated exactly on success.""" + + result: ScheduleTest | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SchedulerInfo(ServiceInfo): + """Supported schedule actions, clocks, limits and retry policy.""" + + ownership: OwnershipCapabilities + action_kinds: Annotated[list[str], non_null_list(pa.string())] + output_formats: Annotated[list[str], non_null_list(pa.string())] + relative_tokens: Annotated[list[str], non_null_list(pa.string())] + tzdb_version: str + retry_policy: RetryPolicy + alert_events: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + + +class SchedulesProtocol(Protocol): + """Proposed vgi.schedules.v1 records and RPC interface; implementations supply all methods. + + OwnershipCapabilities defines all list/scan scope semantics. Exact owner + cannot be combined with a non-all ownership_scope. parent_owner matches + only the recorded direct management parent; folder recursion never expands + directory ancestry. Filters select authorized resources and never grant access. + """ + + protocol_name: ClassVar[str] = "vgi.schedules.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read(table="info") + def get_scheduler_info(self) -> SchedulerInfo: + """Action kinds, output formats, minimum interval, relative tokens, limits.""" + ... + + @sql_read() + def preview_trigger(self, trigger: Trigger, after: Instant, count: int = 10) -> TriggerPreview: + """Next N fire times, a plain-English description, DST warnings.""" + ... + + @sql_read(row_type=ScheduleSummary, table="schedules") + def list_schedules( + self, + action_kind: str = "", + report_id: str = "", + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + execution_principal_id: str | None = None, + query: str = "", + enabled: bool | None = None, + issue_kind: str = "", + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Summaries; filters combine with AND, a non-all ownership_scope cannot be combined with owner. + + query is a literal title substring; issue_kind selects a current issue. + Orders: id, title, updated_desc, with schedule_id as stable tie-breaker. + limit bounds the whole stream; continuation preserves snapshot and filters. + """ + ... + + @sql_read(row_type=ScheduleRecord) + def scan_schedules( + self, + action_kind: str = "", + report_id: str = "", + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + execution_principal_id: str | None = None, + query: str = "", + enabled: bool | None = None, + issue_kind: str = "", + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Full definitions for explicit exports, with exactly the list filters and ordering.""" + ... + + @sql_read() + def get_schedule(self, schedule_id: str) -> ScheduleRecord: + """Read.""" + ... + + def create_schedule( + self, + request_id: str, + schedule: Schedule, + ownership: OwnershipInput | None = None, + execution: ExecutionInput | None = None, + ) -> ScheduleRecord: + """Create schedule. + + Ownership and execution identity are independently resolved by the host; grant_required if the execution + principal lacks a grant for a required location. + """ + ... + + def update_schedule( + self, schedule_id: str, expected_version: int, request_id: str, schedule: Schedule + ) -> ScheduleRecord: + """Includes pausing and resuming via enabled.""" + ... + + def delete_schedule(self, schedule_id: str, expected_version: int, request_id: str) -> Ack: + """Delete schedule.""" + ... + + def validate_schedule(self, schedule: Schedule, execution: ExecutionInput | None = None) -> ValidationResult: + """Check structure, policy and source readiness without user SQL or rendering; incomplete is not valid.""" + ... + + def start_test_run( + self, request_id: str, schedule: Schedule, *, execution: ExecutionInput | None = None, as_of: Instant + ) -> SchedulePreviewJob: + """Admit a bounded read-only preview; reject effects before dispatch and send no deliveries.""" + ... + + @sql_read() + def get_test_run(self, job_id: str) -> SchedulePreviewJob: + """Read caller-authorized preview status/results until expiry.""" + ... + + def cancel_test_run(self, job_id: str, expected_version: int, request_id: str) -> SchedulePreviewJob: + """Request best-effort cancellation; a nonterminal result does not confirm cessation.""" + ... + + def run_now(self, schedule_id: str, request_id: str, ignore_condition: bool = False) -> ScheduleRun: + """New manual run; use retry_run to recover an existing run.""" + ... + + @sql_read(row_type=ScheduleRunSummary, table="runs") + def list_runs( + self, + schedule_id: str = "", + status: str = "", + since: Instant | None = None, + before: Instant | None = None, + execution_principal_id: str | None = None, + limit: int | None = None, + ) -> Stream[ProducerState]: + """Summaries ordered by descending (scheduled_for, run_id); since inclusive, before exclusive. + + Execution filters match the frozen principal. Recheck current resource + access on every continuation. Invalid/reversed time ranges are rejected. + """ + ... + + @sql_read() + def get_run(self, run_id: str) -> ScheduleRun: + """Run history.""" + ... + + def cancel_run(self, run_id: str, expected_version: int, request_id: str) -> ScheduleRun: + """Cancel run.""" + ... + + def retry_run(self, run_id: str, expected_version: int, request_id: str) -> ScheduleRun: + """Resume frozen work from a safe checkpoint.""" + ... + + def resolve_run( + self, + run_id: str, + expected_version: int, + request_id: str, + resolutions: Annotated[list[StepResolution], non_null_list(pa.struct(StepResolution.ARROW_SCHEMA))], + note: str, + ) -> ScheduleRun: + """Record worker-verified outcomes; never replay a step.""" + ... + + def set_ownership( + self, schedule_id: str, expected_version: int, request_id: str, ownership: Ownership + ) -> ScheduleRecord: + """Transfer management; preserve data and execution authority.""" + ... + + def set_execution_principal( + self, schedule_id: str, expected_version: int, request_id: str, principal_id: str + ) -> ScheduleRecord: + """Select a separately authorized execution identity for future work.""" + ... + + @sql_read(row_type=RunStep) + def list_run_steps(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized snapshot ordered by step_index; attempts are separate rows.""" + ... + + @sql_read(row_type=StepAttempt) + def list_step_attempts(self, run_id: str, step_index: int, limit: int | None = None) -> Stream[ProducerState]: + """Authorized attempt history in ascending attempt order.""" + ... + + @sql_read(row_type=RecoveryEvent) + def list_recovery_events(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized recovery audit in stable admission order; recheck access on continuation.""" + ... + + @sql_read(row_type=DestinationResult) + def list_run_destinations(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized per-destination outcomes in original dispatch order.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/schedules_v2.md b/docs/design/reporting-protocols/reference/schedules_v2.md new file mode 100644 index 00000000..52aa77da --- /dev/null +++ b/docs/design/reporting-protocols/reference/schedules_v2.md @@ -0,0 +1,451 @@ +# Object schedules: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/schedules_v2.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../schedules.md) · [Shared records](common.md) + +Protocol `vgi.schedules.v2`, version `2.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `cancel_run` | `run_id`, `expected_version`, `request_id` | `ScheduleRunV2` | `result: binary` (IPC record) | — | +| `cancel_test_run` | `job_id`, `expected_version`, `request_id` | `SchedulePreviewJobV2` | `result: binary` (IPC record) | — | +| `create_schedule` | `request_id`, `schedule`, `ownership`, `execution` | `ScheduleRecordV2` | `result: binary` (IPC record) | — | +| `delete_schedule` | `schedule_id`, `expected_version`, `request_id` | `Ack` | `result: binary` (IPC record) | — | +| `get_run` | `run_id` | `ScheduleRunV2` | `result: binary` (IPC record) | Function | +| `get_schedule` | `schedule_id` | `ScheduleRecordV2` | `result: binary` (IPC record) | Function | +| `get_scheduler_info` | — | `SchedulerInfoV2` | `result: binary` (IPC record) | Function; table `info` | +| `get_test_run` | `job_id` | `SchedulePreviewJobV2` | `result: binary` (IPC record) | Function | +| `list_recovery_events` | `run_id`, `limit` | `RecoveryEvent` | Producer batches | Function | +| `list_run_destinations` | `run_id`, `limit` | `DestinationResult` | Producer batches | Function | +| `list_run_steps` | `run_id`, `limit` | `RunStep` | Producer batches | Function | +| `list_runs` | `schedule_id`, `status`, `since`, `before`, `execution_principal_id`, `limit` | `ScheduleRunSummaryV2` | Producer batches | Function; table `runs` | +| `list_schedules` | `action_kind`, `source`, `ownership_scope`, `owner`, `parent_owner`, `execution_principal_id`, `query`, `enabled`, `issue_kind`, `limit`, `order_by` | `ScheduleSummaryV2` | Producer batches | Function; table `schedules` | +| `list_step_attempts` | `run_id`, `step_index`, `limit` | `StepAttempt` | Producer batches | Function | +| `preview_trigger` | `trigger`, `after`, `count` | `TriggerPreview` | `result: binary` (IPC record) | Function | +| `resolve_run` | `run_id`, `expected_version`, `request_id`, `resolutions`, `note` | `ScheduleRunV2` | `result: binary` (IPC record) | — | +| `retry_run` | `run_id`, `expected_version`, `request_id` | `ScheduleRunV2` | `result: binary` (IPC record) | — | +| `run_now` | `schedule_id`, `request_id`, `ignore_condition` | `ScheduleRunV2` | `result: binary` (IPC record) | — | +| `scan_schedules` | `action_kind`, `source`, `ownership_scope`, `owner`, `parent_owner`, `execution_principal_id`, `query`, `enabled`, `issue_kind`, `limit`, `order_by` | `ScheduleRecordV2` | Producer batches | Function | +| `set_execution_principal` | `schedule_id`, `expected_version`, `request_id`, `principal_id` | `ScheduleRecordV2` | `result: binary` (IPC record) | — | +| `set_ownership` | `schedule_id`, `expected_version`, `request_id`, `ownership` | `ScheduleRecordV2` | `result: binary` (IPC record) | — | +| `start_test_run` | `request_id`, `schedule`, `execution`, `as_of` | `SchedulePreviewJobV2` | `result: binary` (IPC record) | — | +| `update_schedule` | `schedule_id`, `expected_version`, `request_id`, `schedule` | `ScheduleRecordV2` | `result: binary` (IPC record) | — | +| `validate_schedule` | `schedule`, `execution` | `ValidationResult` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.schedules.v2 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import Instant, Json, non_null_list +from vgi.reporting._enums import InlineMode, OutputFormat +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import ( + Ack, + CredentialStatus, + DataSource, + Destination, + DestinationResult, + ExecutionIdentity, + ExecutionInput, + Notification, + Output, + OwnerKey, + Ownership, + OwnershipCapabilities, + OwnershipInput, + OwnershipScope, + ParamValue, + PreviewJob, + RecoveryEvent, + ResourceMeta, + RetryPolicy, + Run, + RunStep, + RunSummary, + ServiceInfo, + StepAttempt, + StepResolution, + Trigger, + TriggerPreview, + ValidationResult, +) +from vgi.reporting.sources import ObjectRef, QuerySource, ResolvedSource, RevisionSelection + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleRunSummaryV2(RunSummary): + """Run status associated with its schedule, without the frozen definition.""" + + schedule_id: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleRunV2(Run): + """ScheduleV2 run with its frozen definition and selected report revision.""" + + schedule_id: str + schedule: ScheduleV2 + resolved_sources: Annotated[list[ResolvedSource], non_null_list(pa.struct(ResolvedSource.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RenderReportActionV2(ArrowSerializableDataclass): + """Pinned or tracked report rendering action.""" + + source: ObjectRef + selection: RevisionSelection = field(default_factory=lambda: RevisionSelection(mode="published")) + outputs: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=lambda: ["application/pdf"]) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RunQueryActionV2(ArrowSerializableDataclass): + """SQL setup/query action and requested output format.""" + + source: QuerySource + output_format: OutputFormat = "parquet" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ActionV2(ArrowSerializableDataclass): + """Tagged action; only the selected payload is populated. + + data_sources belongs to condition/vendor SQL. A query source supplies its own + bindings and public parameters; conflicting attachment aliases are invalid. + """ + + kind: str + render_report: RenderReportActionV2 | None = None + run_query: RunQueryActionV2 | None = None + data_sources: Annotated[list[DataSource], non_null_list(pa.struct(DataSource.ARROW_SCHEMA))] = field( + default_factory=list + ) + custom_json: Json | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DeliveryV2(ArrowSerializableDataclass): + """Destinations and presentation preferences for one action delivery.""" + + destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))] + inline: InlineMode = "summary" + attach: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleAlertsV2(ArrowSerializableDataclass): + """Separate, worker-validated recipients for advertised schedule alert events. + + The worker owns warning thresholds, incident grouping and delivery policy. + Report delivery recipients are never implicitly subscribed to alerts. + """ + + destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))] + events: Annotated[list[str], non_null_list(pa.string())] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleIssueV2(ArrowSerializableDataclass): + """Current worker-reported attention item, without credentials or raw errors.""" + + kind: str + message: str + run_id: str | None = None + expires_at: Instant | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleV2(ArrowSerializableDataclass): + """Editable schedule definition; ownership and execution identity are separate.""" + + title: str + trigger: Trigger + action: ActionV2 + condition_sql: str = "" + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] = field( + default_factory=list + ) + deliveries: Annotated[list[DeliveryV2], non_null_list(pa.struct(DeliveryV2.ARROW_SCHEMA))] = field( + default_factory=list + ) + enabled: bool = True + alerts: ScheduleAlertsV2 | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleRecordV2(ResourceMeta): + """Persistent schedule definition, management identity and current status.""" + + schedule_id: str + definition: ScheduleV2 + execution_identity: ExecutionIdentity + next_fire_at: Instant | None + last_run: ScheduleRunSummaryV2 | None + disabled_reason: str | None + credentials: Annotated[list[CredentialStatus], non_null_list(pa.struct(CredentialStatus.ARROW_SCHEMA))] + issues: Annotated[list[ScheduleIssueV2], non_null_list(pa.struct(ScheduleIssueV2.ARROW_SCHEMA))] = field( + default_factory=list + ) + last_successful_at: Instant | None = None + last_delivery_at: Instant | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleSummaryV2(ResourceMeta): + """Browseable schedule status without SQL, recipients, credentials or histories.""" + + schedule_id: str + title: str + action_kind: str + enabled: bool + execution_identity: ExecutionIdentity + next_fire_at: Instant | None + last_run: ScheduleRunSummaryV2 | None + disabled_reason: str | None + issues: Annotated[list[ScheduleIssueV2], non_null_list(pa.struct(ScheduleIssueV2.ARROW_SCHEMA))] + last_successful_at: Instant | None + last_delivery_at: Instant | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScheduleTestV2(ArrowSerializableDataclass): + """Safe preview results and prospective messages without delivery.""" + + evaluated_at: Instant + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] + condition_value: bool | None + condition_evaluated: bool + outputs: Annotated[list[Output], non_null_list(pa.struct(Output.ARROW_SCHEMA))] + messages: Annotated[list[Notification], non_null_list(pa.struct(Notification.ARROW_SCHEMA))] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SchedulePreviewJobV2(PreviewJob): + """Typed preview result, populated exactly on success.""" + + result: ScheduleTestV2 | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SchedulerInfoV2(ServiceInfo): + """Supported schedule actions, clocks, limits and retry policy.""" + + ownership: OwnershipCapabilities + action_kinds: Annotated[list[str], non_null_list(pa.string())] + output_formats: Annotated[list[str], non_null_list(pa.string())] + relative_tokens: Annotated[list[str], non_null_list(pa.string())] + tzdb_version: str + retry_policy: RetryPolicy + alert_events: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + + +class SchedulesProtocolV2(Protocol): + """Proposed vgi.schedules.v2 records and RPC interface; implementations supply all methods. + + OwnershipCapabilities defines all list/scan scope semantics. Exact owner + cannot be combined with a non-all ownership_scope. parent_owner matches + only the recorded direct management parent; folder recursion never expands + directory ancestry. Filters select authorized resources and never grant access. + """ + + protocol_name: ClassVar[str] = "vgi.schedules.v2" + protocol_version: ClassVar[str] = "2.0.0" + + @sql_read(table="info") + def get_scheduler_info(self) -> SchedulerInfoV2: + """ActionV2 kinds, output formats, minimum interval, relative tokens, limits.""" + ... + + @sql_read() + def preview_trigger(self, trigger: Trigger, after: Instant, count: int = 10) -> TriggerPreview: + """Next N fire times, a plain-English description, DST warnings.""" + ... + + @sql_read(row_type=ScheduleSummaryV2, table="schedules") + def list_schedules( + self, + action_kind: str = "", + source: ObjectRef | None = None, + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + execution_principal_id: str | None = None, + query: str = "", + enabled: bool | None = None, + issue_kind: str = "", + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Summaries; filters combine with AND, a non-all ownership_scope cannot be combined with owner. + + query is a literal title substring; issue_kind selects a current issue. + Orders: id, title, updated_desc, with schedule_id as stable tie-breaker. + limit bounds the whole stream; continuation preserves snapshot and filters. + """ + ... + + @sql_read(row_type=ScheduleRecordV2) + def scan_schedules( + self, + action_kind: str = "", + source: ObjectRef | None = None, + ownership_scope: OwnershipScope = "all", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + execution_principal_id: str | None = None, + query: str = "", + enabled: bool | None = None, + issue_kind: str = "", + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Full definitions for explicit exports, with exactly the list filters and ordering.""" + ... + + @sql_read() + def get_schedule(self, schedule_id: str) -> ScheduleRecordV2: + """Read.""" + ... + + def create_schedule( + self, + request_id: str, + schedule: ScheduleV2, + ownership: OwnershipInput | None = None, + execution: ExecutionInput | None = None, + ) -> ScheduleRecordV2: + """Create schedule. + + Ownership and execution identity are independently resolved by the host; grant_required if the execution + principal lacks a grant for a required location. + """ + ... + + def update_schedule( + self, schedule_id: str, expected_version: int, request_id: str, schedule: ScheduleV2 + ) -> ScheduleRecordV2: + """Includes pausing and resuming via enabled.""" + ... + + def delete_schedule(self, schedule_id: str, expected_version: int, request_id: str) -> Ack: + """Delete schedule.""" + ... + + def validate_schedule(self, schedule: ScheduleV2, execution: ExecutionInput | None = None) -> ValidationResult: + """Check structure, policy and source readiness without user SQL or rendering; incomplete is not valid.""" + ... + + def start_test_run( + self, request_id: str, schedule: ScheduleV2, *, execution: ExecutionInput | None = None, as_of: Instant + ) -> SchedulePreviewJobV2: + """Admit a bounded read-only preview; reject effects before dispatch and send no deliveries.""" + ... + + @sql_read() + def get_test_run(self, job_id: str) -> SchedulePreviewJobV2: + """Read caller-authorized preview status/results until expiry.""" + ... + + def cancel_test_run(self, job_id: str, expected_version: int, request_id: str) -> SchedulePreviewJobV2: + """Request best-effort cancellation; a nonterminal result does not confirm cessation.""" + ... + + def run_now(self, schedule_id: str, request_id: str, ignore_condition: bool = False) -> ScheduleRunV2: + """New manual run; use retry_run to recover an existing run.""" + ... + + @sql_read(row_type=ScheduleRunSummaryV2, table="runs") + def list_runs( + self, + schedule_id: str = "", + status: str = "", + since: Instant | None = None, + before: Instant | None = None, + execution_principal_id: str | None = None, + limit: int | None = None, + ) -> Stream[ProducerState]: + """Summaries ordered by descending (scheduled_for, run_id); since inclusive, before exclusive. + + Execution filters match the frozen principal. Recheck current resource + access on every continuation. Invalid/reversed time ranges are rejected. + """ + ... + + @sql_read() + def get_run(self, run_id: str) -> ScheduleRunV2: + """Run history.""" + ... + + def cancel_run(self, run_id: str, expected_version: int, request_id: str) -> ScheduleRunV2: + """Cancel run.""" + ... + + def retry_run(self, run_id: str, expected_version: int, request_id: str) -> ScheduleRunV2: + """Resume frozen work from a safe checkpoint.""" + ... + + def resolve_run( + self, + run_id: str, + expected_version: int, + request_id: str, + resolutions: Annotated[list[StepResolution], non_null_list(pa.struct(StepResolution.ARROW_SCHEMA))], + note: str, + ) -> ScheduleRunV2: + """Record worker-verified outcomes; never replay a step.""" + ... + + def set_ownership( + self, schedule_id: str, expected_version: int, request_id: str, ownership: Ownership + ) -> ScheduleRecordV2: + """Transfer management; preserve data and execution authority.""" + ... + + def set_execution_principal( + self, schedule_id: str, expected_version: int, request_id: str, principal_id: str + ) -> ScheduleRecordV2: + """Select a separately authorized execution identity for future work.""" + ... + + @sql_read(row_type=RunStep) + def list_run_steps(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized snapshot ordered by step_index; attempts are separate rows.""" + ... + + @sql_read(row_type=StepAttempt) + def list_step_attempts(self, run_id: str, step_index: int, limit: int | None = None) -> Stream[ProducerState]: + """Authorized attempt history in ascending attempt order.""" + ... + + @sql_read(row_type=RecoveryEvent) + def list_recovery_events(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized recovery audit in stable admission order; recheck access on continuation.""" + ... + + @sql_read(row_type=DestinationResult) + def list_run_destinations(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized per-destination outcomes in original dispatch order.""" + ... +``` diff --git a/docs/design/reporting-protocols/reference/sources.md b/docs/design/reporting-protocols/reference/sources.md new file mode 100644 index 00000000..a88ce4a3 --- /dev/null +++ b/docs/design/reporting-protocols/reference/sources.md @@ -0,0 +1,253 @@ +# Portable sources: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/sources.py`. Edit the Python module and regenerate this page. + +## Arrow aliases and string choices + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Explicit Arrow encodings shared by the proposed reporting contracts.""" + +from datetime import datetime +from typing import Annotated + +import pyarrow as pa +from vgi_rpc import ArrowType + +Instant = Annotated[datetime, ArrowType(pa.timestamp("us", "UTC"))] +"""An aware datetime, encoded as UTC microseconds on the wire.""" + +Json = str +"""One JSON value encoded as text; semantic validation belongs to the worker.""" + +SchemaIpc = Annotated[pa.Schema, ArrowType(pa.binary())] +"""An Arrow schema serialized as one IPC schema message.""" + +RowIpc = Annotated[pa.RecordBatch, ArrowType(pa.binary())] +"""A one-row batch serialized with its schema as a complete IPC stream.""" + + +def non_null_list(item: pa.DataType) -> ArrowType: + """Declare a list whose elements cannot be null, independent of Arrow defaults.""" + return ArrowType(pa.list_(pa.field("item", item, nullable=False))) +``` + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Known string choices, encoded as utf8 rather than dictionary-encoded Enum. + +Literal annotations assist callers and reviewers. Workers enforce allowed input +values; clients must still tolerate unknown output values from newer workers. +Plain strings are retained for explicitly open vendor kinds and action hints. +""" + +from typing import Annotated, Literal + +import pyarrow as pa +from vgi_rpc import ArrowType + +ExecutionState = Annotated[Literal["ready", "needs_credentials", "unavailable"], ArrowType(pa.string())] +CredentialState = Annotated[Literal["ok", "expiring", "expired", "missing"], ArrowType(pa.string())] +LimitUnit = Annotated[Literal["bytes", "rows", "seconds", "count", "utf8_bytes"], ArrowType(pa.string())] +ParameterType = Annotated[ + Literal["text", "number", "date", "boolean", "select", "multi_select", "date_range"], ArrowType(pa.string()) +] +ParamKind = Annotated[Literal["literal", "relative"], ArrowType(pa.string())] +Severity = Annotated[Literal["info", "warning", "critical"], ArrowType(pa.string())] +DeliveryStatus = Annotated[Literal["accepted", "refused", "failed", "unknown"], ArrowType(pa.string())] +TriggerKind = Annotated[Literal["cron", "once"], ArrowType(pa.string())] +EffectOutcome = Annotated[Literal["none", "committed", "rolled_back", "partial", "unknown"], ArrowType(pa.string())] +ResolvedOutcome = Annotated[Literal["committed", "rolled_back", "partial"], ArrowType(pa.string())] +StepStatus = Annotated[Literal["pending", "running", "succeeded", "failed", "skipped"], ArrowType(pa.string())] +RecoveryState = Annotated[ + Literal[ + "none", "retry_scheduled", "retryable", "needs_reauthorization", "needs_resolution", "exhausted", "resolved" + ], + ArrowType(pa.string()), +] +RunStatus = Annotated[ + Literal["pending", "running", "retry_wait", "succeeded", "skipped", "failed", "cancelled"], ArrowType(pa.string()) +] +RunTriggerKind = Annotated[Literal["schedule", "manual", "test"], ArrowType(pa.string())] +RevisionKind = Annotated[Literal["edit", "agent", "restore", "import"], ArrowType(pa.string())] +RenderStatus = Annotated[Literal["queued", "running", "succeeded", "failed", "cancelled"], ArrowType(pa.string())] +ReportTracking = Annotated[Literal["pinned", "published", "head"], ArrowType(pa.string())] +OutputFormat = Annotated[Literal["none", "arrow", "parquet", "csv"], ArrowType(pa.string())] +InlineMode = Annotated[Literal["none", "summary", "image", "table", "report"], ArrowType(pa.string())] +TargetKind = Annotated[Literal["catalog", "host"], ArrowType(pa.string())] +LoadMode = Annotated[Literal["replace", "append", "merge", "snapshot"], ArrowType(pa.string())] +SchemaChangePolicy = Annotated[Literal["fail", "add_columns"], ArrowType(pa.string())] +TransactionMode = Annotated[Literal["single", "per_statement", "none"], ArrowType(pa.string())] +IncrementalKind = Annotated[Literal["none", "derived", "stored"], ArrowType(pa.string())] +HealthState = Annotated[Literal["healthy", "failing", "stale", "paused", "disabled", "new"], ArrowType(pa.string())] +AlertRuleState = Annotated[Literal["ok", "firing", "error", "disabled"], ArrowType(pa.string())] +AlertInstanceState = Annotated[ + Literal["pending", "firing", "resolving", "resolved", "superseded"], ArrowType(pa.string()) +] +AlertEventKind = Annotated[ + Literal[ + "transition", + "notification", + "acknowledged", + "unacknowledged", + "snoozed", + "unsnoozed", + "subscribed", + "unsubscribed", + "rule_error", + "rule_recovered", + "superseded", + ], + ArrowType(pa.string()), +] +DelegationKind = Annotated[Literal["catalog", "service"], ArrowType(pa.string())] +``` + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Portable saved-source identities, query definitions and revision selection.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, Literal + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType + +from vgi.reporting._arrow import Json, non_null_list +from vgi.reporting.common import DataSource, ParameterSpec + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ObjectRef(ArrowSerializableDataclass): + """Object identity; service_url is the normalized complete service location.""" + + service_url: str + object_id: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RevisionSelection(ArrowSerializableDataclass): + """Exactly pinned carries a nonempty revision_id; floating modes carry None.""" + + mode: Annotated[Literal["head", "published", "pinned"], ArrowType(pa.string())] + revision_id: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ResolvedSource(ArrowSerializableDataclass): + """Frozen source provenance, without credentials or document content.""" + + source: ObjectRef + revision_id: str + body_sha256: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ExecutionContext(ArrowSerializableDataclass): + """Portable name resolution. Alias must identify a declared source attachment. + + None means the engine's isolated local database, never the recipient's + workspace default. A schema without a source alias is invalid. + """ + + default_source_alias: str | None = None + default_schema: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class QueryDefinition(ArrowSerializableDataclass): + """The JSON body of cupola.query/1; bindings and parameters live in its envelope.""" + + query: str + setup_sql: str = "" + dialect: str = "duckdb" + context: ExecutionContext = field(default_factory=ExecutionContext) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class InlineQuery(ArrowSerializableDataclass): + """Inline equivalent of a saved query revision's body and public inputs.""" + + definition: QueryDefinition + data_sources: Annotated[list[DataSource], non_null_list(pa.struct(DataSource.ARROW_SCHEMA))] = field( + default_factory=list + ) + parameters: Annotated[list[ParameterSpec], non_null_list(pa.struct(ParameterSpec.ARROW_SCHEMA))] = field( + default_factory=list + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class QuerySource(ArrowSerializableDataclass): + """Exactly one of inline or saved; selection is required exactly for saved.""" + + inline: InlineQuery | None = None + saved: ObjectRef | None = None + selection: RevisionSelection | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SourceProgressDecision(ArrowSerializableDataclass): + """Explicit source-change decision; workers validate authority and target safety. + + reset_watermark_json is used only for reset of stored progress; None clears + it. rebuild gates ordinary admission until confirmed full-refresh completion. + """ + + mode: Annotated[Literal["retain", "reset", "rebuild"], ArrowType(pa.string())] + reset_watermark_json: Json | None = None + + +def validate_selection(selection: RevisionSelection) -> None: + """Reject malformed selectors before resolution or dispatch.""" + if selection.mode not in {"head", "published", "pinned"}: + raise ValueError("Unknown revision selection mode") + if (selection.mode == "pinned" and not selection.revision_id) or ( + selection.mode != "pinned" and selection.revision_id is not None + ): + raise ValueError("Only pinned selection requires a nonempty revision ID") + + +def validate_query_source(source: QuerySource, *, incremental: bool = False) -> None: + """Validate exclusive source payloads and pinned incremental-source admission.""" + if (source.inline is None) == (source.saved is None): + raise ValueError("Choose exactly one inline or saved query") + if source.saved is not None: + if not source.saved.service_url or not source.saved.object_id or source.selection is None: + raise ValueError("Saved queries require service, object and revision selection") + validate_selection(source.selection) + if incremental and source.selection.mode != "pinned": + raise ValueError("Incremental tasks require pinned saved-query revisions") + elif source.selection is not None: + raise ValueError("Inline queries do not accept revision selection") + if source.inline is not None: + context = source.inline.definition.context + aliases = [item.alias for item in source.inline.data_sources] + if len(aliases) != len(set(aliases)): + raise ValueError("Source attachment aliases must be distinct") + if context.default_source_alias is not None and context.default_source_alias not in aliases: + raise ValueError("Default source alias must name a declared attachment") + if context.default_schema is not None and (not context.default_source_alias or not context.default_schema): + raise ValueError("A nonempty default schema requires a source alias") + + +def validate_progress_decision(decision: SourceProgressDecision | None, *, incremental_kind: str) -> None: + """Require a compatible explicit decision when an incremental source changes.""" + if decision is None or decision.mode not in {"retain", "reset", "rebuild"}: + raise ValueError("Changing an incremental source requires retain, reset or rebuild") + if decision.mode == "reset" and incremental_kind != "stored": + raise ValueError("Derived progress must be reset through a full rebuild") + if decision.mode != "reset" and decision.reset_watermark_json is not None: + raise ValueError("Only reset accepts a watermark value") +``` diff --git a/docs/design/reporting-protocols/reference/sql_tasks.md b/docs/design/reporting-protocols/reference/sql_tasks.md new file mode 100644 index 00000000..4a14a863 --- /dev/null +++ b/docs/design/reporting-protocols/reference/sql_tasks.md @@ -0,0 +1,522 @@ +# SQL tasks: Python reference + + + +[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions) + +Source: `vgi/reporting/sql_tasks.py`. Edit the Python module and regenerate this page. + +[Behavior specification](../sql_tasks.md) · [Shared records](common.md) + +Protocol `vgi.sql_tasks.v1`, version `1.0.0`. + +## Methods + +| Method | Arguments | Result / stream row | RPC result envelope | SQL read | +| --- | --- | --- | --- | --- | +| `cancel_run` | `run_id`, `expected_version`, `request_id` | `TaskRun` | `result: binary` (IPC record) | — | +| `cancel_test_run` | `job_id`, `expected_version`, `request_id` | `TaskPreviewJob` | `result: binary` (IPC record) | — | +| `create_task` | `request_id`, `task`, `ownership`, `execution` | `TaskRecord` | `result: binary` (IPC record) | — | +| `delete_task` | `task_id`, `expected_version`, `request_id`, `drop_target` | `Ack` | `result: binary` (IPC record) | — | +| `get_run` | `run_id` | `TaskRun` | `result: binary` (IPC record) | Function | +| `get_task` | `task_id` | `TaskRecord` | `result: binary` (IPC record) | Function | +| `get_tasks_info` | — | `TasksInfo` | `result: binary` (IPC record) | Function; table `info` | +| `get_test_run` | `job_id` | `TaskPreviewJob` | `result: binary` (IPC record) | Function | +| `list_recovery_events` | `run_id`, `limit` | `RecoveryEvent` | Producer batches | Function | +| `list_run_destinations` | `run_id`, `limit` | `DestinationResult` | Producer batches | Function | +| `list_run_steps` | `run_id`, `limit` | `RunStep` | Producer batches | Function | +| `list_runs` | `task_id`, `status`, `since`, `before`, `execution_principal_id`, `limit` | `TaskRunSummary` | Producer batches | Function; table `runs` | +| `list_step_attempts` | `run_id`, `step_index`, `limit` | `StepAttempt` | Producer batches | Function | +| `list_tasks` | `ownership_scope`, `target_id`, `health_state`, `owner`, `parent_owner`, `execution_principal_id`, `query`, `enabled`, `limit`, `order_by` | `TaskSummary` | Producer batches | Function; table `tasks` | +| `preview_trigger` | `trigger`, `after`, `count` | `TriggerPreview` | `result: binary` (IPC record) | Function | +| `resolve_run` | `run_id`, `expected_version`, `request_id`, `resolutions`, `note` | `TaskRun` | `result: binary` (IPC record) | — | +| `retry_run` | `run_id`, `expected_version`, `request_id` | `TaskRun` | `result: binary` (IPC record) | — | +| `run_now` | `task_id`, `request_id`, `ignore_condition`, `full_refresh` | `TaskRun` | `result: binary` (IPC record) | — | +| `scan_tasks` | `ownership_scope`, `target_id`, `health_state`, `owner`, `parent_owner`, `execution_principal_id`, `query`, `enabled`, `limit`, `order_by` | `TaskRecord` | Producer batches | Function | +| `set_execution_principal` | `task_id`, `expected_version`, `request_id`, `principal_id` | `TaskRecord` | `result: binary` (IPC record) | — | +| `set_ownership` | `task_id`, `expected_version`, `request_id`, `ownership` | `TaskRecord` | `result: binary` (IPC record) | — | +| `set_watermark` | `task_id`, `expected_version`, `request_id`, `watermark_json` | `TaskRecord` | `result: binary` (IPC record) | — | +| `start_test_run` | `request_id`, `task`, `execution`, `as_of` | `TaskPreviewJob` | `result: binary` (IPC record) | — | +| `update_task` | `task_id`, `expected_version`, `request_id`, `task`, `progress` | `TaskRecord` | `result: binary` (IPC record) | — | +| `validate_task` | `task`, `execution` | `ValidationResult` | `result: binary` (IPC record) | — | + +## Python definitions + +```python +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Proposed vgi.sql_tasks.v1 records and RPC interface. + +These are importable protocol definitions, not a service implementation. +Behavior and worker policy are documented in docs/design/reporting-protocols/. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Annotated, ClassVar, Protocol + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import ProducerState, Stream + +from vgi.reporting._arrow import Instant, Json, SchemaIpc, non_null_list +from vgi.reporting._enums import HealthState, IncrementalKind, LoadMode, SchemaChangePolicy, TargetKind, TransactionMode +from vgi.reporting._metadata import sql_read +from vgi.reporting.common import ( + Ack, + CredentialStatus, + DataSource, + Destination, + DestinationResult, + ExecutionIdentity, + ExecutionInput, + OwnerKey, + Ownership, + OwnershipCapabilities, + OwnershipInput, + OwnershipScope, + ParamValue, + PreviewJob, + RecoveryEvent, + ResourceMeta, + RetryPolicy, + Run, + RunError, + RunStep, + RunSummary, + ServiceInfo, + StepAttempt, + StepResolution, + Trigger, + TriggerPreview, + ValidationResult, +) +from vgi.reporting.sources import QuerySource, ResolvedSource, SourceProgressDecision + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskRunSummary(RunSummary): + """Run status associated with a task without its frozen SQL definition.""" + + task_id: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskRun(Run): + """Task run with frozen inputs, load counts and typed watermark JSON.""" + + resolved_sources: Annotated[list[ResolvedSource], non_null_list(pa.struct(ResolvedSource.ARROW_SCHEMA))] = field( + default_factory=list + ) + task_id: str + task: SqlTask + full_refresh: bool + watermark_before: Json | None + watermark_after: Json | None + rows_inserted: int | None + rows_updated: int | None + rows_deleted: int | None + target_row_count: int | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Target(ArrowSerializableDataclass): + """Authorized catalog table or host target for a load.""" + + kind: TargetKind + alias: str = "" + schema: str = "main" + table: str + create_if_missing: bool = True + + +@dataclass(frozen=True, slots=True, kw_only=True) +class LoadBody(ArrowSerializableDataclass): + """Read-only source query and transactional load configuration.""" + + source: QuerySource + target: Target + mode: LoadMode + key_columns: Annotated[list[str], non_null_list(pa.string())] = field(default_factory=list) + delete_missing: bool = False + snapshot_column: str = "_snapshot_at" + retain_snapshots: int = 0 + on_schema_change: SchemaChangePolicy = "fail" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ScriptBody(ArrowSerializableDataclass): + """Ordered statements and explicit transaction/watermark behavior.""" + + statements: Annotated[list[str], non_null_list(pa.string())] + transaction: TransactionMode = "single" + watermark_sql: str = "" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskBody(ArrowSerializableDataclass): + """Tagged load, script or vendor body; unused payloads are None.""" + + kind: str + load: LoadBody | None = None + script: ScriptBody | None = None + custom_json: Json | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Incremental(ArrowSerializableDataclass): + """Watermark mode, typed initial value and optional fixed-duration lookback.""" + + kind: IncrementalKind = "none" + cursor_column: str = "" + initial_json: Json | None = None + lookback: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskNotify(ArrowSerializableDataclass): + """Notification destinations and selected task health transitions.""" + + destinations: Annotated[list[Destination], non_null_list(pa.struct(Destination.ARROW_SCHEMA))] = field( + default_factory=list + ) + on: Annotated[list[str], non_null_list(pa.string())] = field( + default_factory=lambda: ["failing", "recovered", "disabled"] + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SqlTask(ArrowSerializableDataclass): + """Editable task; incremental saved-query loads must use pinned selection. + + data_sources holds task-owned target/condition bindings; a saved source owns + its own query bindings. Conflicting aliases are rejected, never substituted. + """ + + title: str + description: str = "" + trigger: Trigger + data_sources: Annotated[list[DataSource], non_null_list(pa.struct(DataSource.ARROW_SCHEMA))] + body: TaskBody + incremental: Incremental = field(default_factory=Incremental) + condition_sql: str = "" + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] = field( + default_factory=list + ) + notify: TaskNotify = field(default_factory=TaskNotify) + stale_after_seconds: int = 0 + timeout_seconds: int = 3600 + enabled: bool = True + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskHealth(ArrowSerializableDataclass): + """Current task health and most recent success/failure observations.""" + + state: HealthState + since: Instant + last_success_at: Instant | None + last_failure_at: Instant | None + last_error: RunError | None + consecutive_failures: int + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ResolvedTarget(ArrowSerializableDataclass): + """Stable target identity that survives management ownership changes.""" + + target_id: str + location: str + catalog_name: str + schema: str + table: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskRecord(ResourceMeta): + """Persistent task definition, target mapping, progress and health.""" + + task_id: str + definition: SqlTask + accepted_source: ResolvedSource | None = None + pending_full_refresh: bool = False + execution_identity: ExecutionIdentity + next_fire_at: Instant | None + last_run: TaskRunSummary | None + watermark_json: Json | None + target: ResolvedTarget | None + target_row_count: int | None + disabled_reason: str | None + credentials: Annotated[list[CredentialStatus], non_null_list(pa.struct(CredentialStatus.ARROW_SCHEMA))] + health: TaskHealth + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskSummary(ResourceMeta): + """Task management and health, without SQL, source bindings or histories.""" + + task_id: str + title: str + enabled: bool + execution_identity: ExecutionIdentity + next_fire_at: Instant | None + last_run: TaskRunSummary | None + target: ResolvedTarget | None + pending_full_refresh: bool + health: TaskHealth + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskTest(ArrowSerializableDataclass): + """Rollback-only test observations; unknown counts/schema are None.""" + + evaluated_at: Instant + parameter_values: Annotated[list[ParamValue], non_null_list(pa.struct(ParamValue.ARROW_SCHEMA))] + condition_value: bool | None + condition_evaluated: bool + watermark_before: Json | None + watermark_after: Json | None + rows_inserted: int | None + rows_updated: int | None + rows_deleted: int | None + target_schema: SchemaIpc | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TaskPreviewJob(PreviewJob): + """Typed rollback-only task observations, populated exactly on success.""" + + result: TaskTest | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TasksInfo(ServiceInfo): + """Supported task modes, host catalog, limits and progress guarantees.""" + + ownership: OwnershipCapabilities + body_kinds: Annotated[list[str], non_null_list(pa.string())] + load_modes: Annotated[list[str], non_null_list(pa.string())] + host_location: str | None + host_catalog_name: str | None + atomic_host_watermarks: bool + relative_tokens: Annotated[list[str], non_null_list(pa.string())] + tzdb_version: str + retry_policy: RetryPolicy + + +class SqlTasksProtocol(Protocol): + """Proposed vgi.sql_tasks.v1 records and RPC interface; implementations supply all methods. + + OwnershipCapabilities defines all list/scan scope semantics. Exact owner + cannot be combined with a non-all ownership_scope. parent_owner matches + only the recorded direct management parent; folder recursion never expands + directory ancestry. Filters select authorized resources and never grant access. + """ + + protocol_name: ClassVar[str] = "vgi.sql_tasks.v1" + protocol_version: ClassVar[str] = "1.0.0" + + @sql_read(table="info") + def get_tasks_info(self) -> TasksInfo: + """Get tasks info. + + Body kinds, modes, host store catalog and location, whether host watermarks are atomic, minimum interval, + limits. + """ + ... + + @sql_read() + def preview_trigger(self, trigger: Trigger, after: Instant, count: int = 10) -> TriggerPreview: + """As in schedules.""" + ... + + @sql_read(row_type=TaskSummary, table="tasks") + def list_tasks( + self, + ownership_scope: OwnershipScope = "all", + target_id: str = "", + health_state: str = "", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + execution_principal_id: str | None = None, + query: str = "", + enabled: bool | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """AND-filtered summaries; title substring query and id/title/updated_desc ordering. + + Owner filters are canonical exact keys, a non-all ownership_scope cannot be combined with owner; + limit counts all rows across continuations. Task ID breaks ordering ties. + """ + ... + + @sql_read(row_type=TaskRecord) + def scan_tasks( + self, + ownership_scope: OwnershipScope = "all", + target_id: str = "", + health_state: str = "", + owner: OwnerKey | None = None, + parent_owner: OwnerKey | None = None, + execution_principal_id: str | None = None, + query: str = "", + enabled: bool | None = None, + limit: int | None = None, + order_by: str = "id", + ) -> Stream[ProducerState]: + """Full definition export with the same filters, snapshot and ordering as list_tasks.""" + ... + + @sql_read() + def get_task(self, task_id: str) -> TaskRecord: + """Read, with health; health_state filters ("everything failing").""" + ... + + def create_task( + self, + request_id: str, + task: SqlTask, + ownership: OwnershipInput | None = None, + execution: ExecutionInput | None = None, + ) -> TaskRecord: + """Create task. + + Ownership and execution identity are independently resolved by the host; grant_required for any source + without a usable delegation. + """ + ... + + def update_task( + self, + task_id: str, + expected_version: int, + request_id: str, + task: SqlTask, + progress: SourceProgressDecision | None = None, + ) -> TaskRecord: + """Update with explicit progress choice for changed incremental inputs. + + Source edits require no active run or unresolved effect. A rebuild gates + ordinary admission until its full refresh and progress are confirmed. + Workers validate retain/reset safety; derived progress cannot use reset. + """ + ... + + def delete_task(self, task_id: str, expected_version: int, request_id: str, drop_target: bool = False) -> Ack: + """drop_target drops a host table; a catalog target is never dropped.""" + ... + + def validate_task(self, task: SqlTask, execution: ExecutionInput | None = None) -> ValidationResult: + """Check structure, policy and sources without executing user SQL; incomplete checks are explicit.""" + ... + + def start_test_run( + self, request_id: str, task: SqlTask, *, execution: ExecutionInput | None = None, as_of: Instant + ) -> TaskPreviewJob: + """Admit a bounded rollback-only preview; reject irreversible effects before dispatch. + + Refuse per_statement/none scripts and calls not safely rolled back. + Never change saved progress, adopt a source, or send notifications. + """ + ... + + @sql_read() + def get_test_run(self, job_id: str) -> TaskPreviewJob: + """Read caller-authorized status/results until expiry.""" + ... + + def cancel_test_run(self, job_id: str, expected_version: int, request_id: str) -> TaskPreviewJob: + """Request cancellation and rollback; nonterminal status does not confirm cessation.""" + ... + + def run_now( + self, task_id: str, request_id: str, ignore_condition: bool = False, full_refresh: bool = False + ) -> TaskRun: + """New manual run; use retry_run to recover an existing run. + + full_refresh runs with $watermark = NULL and, for load, replace semantics. + """ + ... + + def set_watermark( + self, task_id: str, expected_version: int, request_id: str, watermark_json: Json | None + ) -> TaskRecord: + """Backfill or reset a stored watermark; null clears it. + + Refused for derived, whose watermark is the target's data. + """ + ... + + @sql_read(row_type=TaskRunSummary, table="runs") + def list_runs( + self, + task_id: str = "", + status: str = "", + since: Instant | None = None, + before: Instant | None = None, + execution_principal_id: str | None = None, + limit: int | None = None, + ) -> Stream[ProducerState]: + """Authorized summaries by descending (scheduled_for, run_id); since inclusive, before exclusive. + + Execution filters match frozen identity. Empty task_id includes every + currently readable task; reversed/empty time windows are invalid. + """ + ... + + @sql_read() + def get_run(self, run_id: str) -> TaskRun: + """Frozen inputs and current run status; growing histories are separate streams.""" + ... + + def cancel_run(self, run_id: str, expected_version: int, request_id: str) -> TaskRun: + """Rolls back an open transaction; per_statement keeps what already committed.""" + ... + + def retry_run(self, run_id: str, expected_version: int, request_id: str) -> TaskRun: + """Resume frozen work from a safe checkpoint.""" + ... + + def resolve_run( + self, + run_id: str, + expected_version: int, + request_id: str, + resolutions: Annotated[list[StepResolution], non_null_list(pa.struct(StepResolution.ARROW_SCHEMA))], + note: str, + ) -> TaskRun: + """Record worker-verified outcomes; never replay a step.""" + ... + + def set_ownership(self, task_id: str, expected_version: int, request_id: str, ownership: Ownership) -> TaskRecord: + """Transfer management; preserve data and execution authority.""" + ... + + def set_execution_principal( + self, task_id: str, expected_version: int, request_id: str, principal_id: str + ) -> TaskRecord: + """Select a separately authorized execution identity for future work.""" + ... + + @sql_read(row_type=RunStep) + def list_run_steps(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized snapshot ordered by step_index; attempts are separate rows.""" + ... + + @sql_read(row_type=StepAttempt) + def list_step_attempts(self, run_id: str, step_index: int, limit: int | None = None) -> Stream[ProducerState]: + """Authorized attempt history in ascending attempt order.""" + ... + + @sql_read(row_type=RecoveryEvent) + def list_recovery_events(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized recovery audit in stable admission order; recheck access on continuation.""" + ... + + @sql_read(row_type=DestinationResult) + def list_run_destinations(self, run_id: str, limit: int | None = None) -> Stream[ProducerState]: + """Authorized per-destination outcomes in original dispatch order.""" + ... +``` diff --git a/docs/protocol/duckdb-2.0-main-integration-2026-10-24.md b/docs/protocol/duckdb-2.0-main-integration-2026-10-24.md new file mode 100644 index 00000000..0d0caf8f --- /dev/null +++ b/docs/protocol/duckdb-2.0-main-integration-2026-10-24.md @@ -0,0 +1,183 @@ +# Latest-main integration — October 24, 2026 + +**Subsequent fix:** the nested-NOT_NULL schema-reconciliation failure recorded +below has been resolved. Native 1.5 now validates and retains nested facts without +making nullable parents NOT NULL. The write regression passes with Python and Go +workers over subprocess and HTTP. The full native 1.5 rerun passed 347 test files, +with 32 skips and zero failures. Current regression and seven-framework evidence +are in `nested_not_null_checkpoint_2026_10_24` in the +[manifest](duckdb-2.0-manifest.json); the original integration results below remain historical. + +Published main is now `vgi.v2` / `2.2.0`. The combined successor remains unpublished and +incompatible despite temporarily carrying those development labels. Its final major version, +routing name and coordinated release gates remain unresolved. The native runtime stays pinned +to Haybarn/DuckDB 1.5 commit `105edd31b57fe7e6d32ee648efad562e45a9f908`. + +This is an integration checkpoint, not a protocol release, full DuckDB 2 qualification or +implementation of automatic whole-query pushdown. Earlier dated source pins, version values +and test results in the manifest remain historical evidence. The original A0 inventory is +retained separately from the refreshed diagnostic build. + +## Exact merged inputs + +These are merge inputs, not final merge-commit IDs. Backup ref: +`codex/pre-main-sync-20261024-kmz7l4hp`. The machine-readable copies are in +`main_integration_checkpoint_2026_10_24` in the [manifest](duckdb-2.0-manifest.json). + +| Repository | Successor branch before merge | Merged main or published tag | +| --- | --- | --- | +| `vgi` | `e1dc7520d28dec8cc7414bb65e227e565b6ff560` | `454fefa50b11f8bfe3065fb11aebf8f5761d7b3d` | +| `vgi-python` | `7dc87f44bb73c723b3cac13f059af50338fdd284` | `fd4f5b97a41972475d93e627c1688b4e75712f6d` | +| `vgi-go` | `ad5f89701ef0bc20845ae95bf1cffe0f8e6cbed8` | `ca85f8cfef3eb8c17bbbb6a13f8a48bf1956670e` | +| `vgi-rust` | `2f0c343ebf010f1cbf6b37f0d084ab8a7ccca338` | `89f3cf651badf1f9caccc89f43f208fa34770e59` | +| `vgi-typescript` | `977b667f49c924f912127eae2c8957d3d0658fa6` | `412384aa56d96d19fdbc88de42a94d994b70ca88` | +| `vgi-java` | `10613b873996fb8bf755a21c80ffd0bab713ec69` | `074ec2fc13973479504bec12f07fb7a597c5343e` | +| `vgi-csharp` | `d77b0b209a8db9fecb220d7d575932d51b54106d` | `15d3cae296d9082e68ad36c058731cb7d8bde7ff` | +| `vgi-cpp` | `1da952dd328578ee9695f50e3ae719e9e57a511f` | `0c5e24522690119bc002f423130be266c8a010a5` | +| `vgi-rpc-go` | `2921723df8246e8715785ffccae4b1a0912d2550` | `688ba348b67c47125953e98ee74847efc279cd87` | +| `vgi-rpc-java` | `30c84da7fc20b38f7c59aae5d2beb5ee483be6a1` | `3a8a300a10226be1b56e0458e81ffa3212b73f1d` (`v0.33.1`) | +| `vgi-rpc-c++` | `08951ccd7ebd4661655765e733388ce371894a7a` | `25fad89ed4bc7746e2958537752a00fe0a707ecd` (`v0.11.0`) | + +The unpublished C# local-main snapshot commit `ce20f46` is covered by upstream `1badcaf` +and subsequent fixes; duplicating it was unnecessary. + +## Reconciled behavior + +- Six catalog-wide Arrow streams preserve canonical constraints, typed index definitions, + macro families, schema options and column metadata. `SchemaInfo` attachment state is + constructor compatibility only. New bulk codecs retain canonical column validation. +- Python capability-projected inventory uses scoped cache/publication keys and etags. + `SnapshotRequest.capability_projection` is internal. A validator for an unprojected + inventory cannot short-circuit a projected request. SDKs that publish unprojected records + retain their existing visibility/version scope; no capability flag or wire field was added. +- Generated C++ codecs and registry integration retain the combined successor methods and + validation. Identity.v2 timestamp APIs and continued table-in-out output coexist with + projection acceptance, repeatability and explicit query execution. +- Java's added bulk-index model now uses canonical definition/options fields. The RPC tag + merge retains annotated String dictionaries alongside upstream binary dictionaries. + +## Tested dependencies + +| SDK | Dependency used for this qualification | +| --- | --- | +| Python | Published `vgi-rpc 0.52.0` from the lockfile | +| TypeScript | Published `vgi-rpc 0.29.2`, Flechette 2.7.1, Arrow 21.1.1 | +| Rust | Published `vgi-rpc`, client and macros 0.32.0 | +| Go | Declared `vgi-rpc-go 0.35.0`; tested isolated merged source with retained framing patch | +| Java | RPC tag 0.33.1 plus retained annotated String dictionary fix | +| C# | Published `QueryFarm.VgiRpc 0.18.1`; old EC2 sibling source bypassed | +| C++ | RPC tag 0.11.0 plus retained strict-decoding fixes | + +Source-backed RPC merges still need coordinated publication/pinning before a release. +The endpoint hash is `41f61f37c7ce42f63f3ca47f8861508f3f356d2422a5b8a0bab48ff92c0857a8` +for 85 methods; it does not fingerprint every nested binary record or semantic metadata rule. + +## Bounded EC2 evidence + +Artifacts: `logs/main-sync-20261024-kmz7l4hp` on the isolated EC2 task root. + +| Lane | Result | +| --- | --- | +| TypeScript | Production build passed; ArrowJS 738 passed/68 skipped, Flechette 739 passed/67 skipped | +| Java | Core 739 passed/2 skipped; examples 61 passed; RPC 681 passed | +| C# | 764 passed against published RPC 0.18.1 | +| Go | 881 passed/1 live-service skip; `go vet` passed | +| Go RPC | 766 passed/17 environment skips; Python-enabled reference subset 36 passed; Python→Go server conformance 1696 passed; `go vet` passed | +| Rust | 664 passed/4 ignored across 45 test binaries | +| C++ | 89/89 CTest cases passed | +| Shared projection/repeatability | 27 cases over subprocess for all six SDKs; HTTP 27 each for Go, TS, Java and C# | +| Native 1.5 contracts | 74 cases/1542 assertions | +| Native 1.5 HTTP, catalog batches, continuation | 8/265, 6/380, 5/473 cases/assertions respectively | +| Native 1.5 discovery/reflection and copy | 8/221 and 2/41 cases/assertions | +| Native 1.5 live references | Repeatability 5, projection 12, explicit-query 18 cases | +| Native six-SDK projection | Each SDK: 5 queries, 42 rows and accepted-plan-copy proof | +| Native 1.5 broad subprocess | 346 passed, 32 skipped, 1 documented pre-existing failure across 379 current test files | + +The native static/loadable extensions, shell, unittest, catalog tests and HTTP tests built. +See `native-merge-evidence.json` for native commands and logs. SDK logs are under `sdk/`. +The rebuilt browser bundle SHA256 is +`2b49411f93ff4f538dcbec6c767c663c56026b11f5e5595c0fb749e4743dc6e8`. + +**Python: 4602 passed, 129 skipped, no failures** (`python-full-final.log`). Mypy passed +235 source files. The HTTP demo failure exposed a port-allocation race in the test fixture: +it now binds before announcing its port, and the harness checks its own child's liveness. +The 19 affected tests passed, including new collision/concurrent-server regressions, then +passed three repeated parallel runs. All-repository generated-artifact checks pass; +the strict manifest check covers 151 audit rows and 20 contract areas with no unresolved +scope/field reviews. Those coverage checks do not complete the release gates. + +**The refreshed 2.0 diagnostic inventory is recorded.** All 110 native C++ sources were +attempted: 54 compiled and 56 failed. Generated `describe.c` also compiled, giving 111 +translation units and 55 successful compilations overall. Diagnostics contain 415 errors +under the per-unit capture cap; these are not a count of independent fixes. The two new +native codec sources compile. New `http_params` fails on `FileOpener` overrides, and +`input_dedup` now fails on removed `DataChunk::SetCapacity`. Other existing source statuses +are unchanged. The original 107-source / 53-compiled / 54-failed inventory remains historical. + +Evidence: native `docs/duckdb-2.0-a0-main-sync.json` and the appended integration section of +`docs/duckdb-2.0-a0-diagnostics.md`. Neither inventory proves linking or runtime support. +The broad 1.5 run exposed the existing `schema_reconcile.test:40` limitation: the adapter +rejects nested constraint column paths such as the fixture's `nested.a` NOT_NULL. A targeted +rerun with a fresh database reproduced it. Both the guard and fixture facts existed before +the merge, and the SQL test is unchanged; the prior failure is already documented in the +native plan. No guard or fixture was weakened. Other failures were not observed. + +The raw broad-run log records 380 files / 347 passed / 32 skipped / 1 failed because it also +ran the retired `parallel_2gb.test_slow` (passed). Main renamed that file to +`parallel_spill_stream.test_slow`; the obsolete copy was archived outside the source tree. +The table reports the 379 current files. Skips require optional services, extensions or +transport-specific fixtures. Logs and the fresh failure reproduction remain available. + +Remaining work is the native 2.0 read port, a general SQL provider with shared mutable snapshot +and active-SQL cancellation proof, automatic eligibility/export/replacement, remaining transport +and platform qualification, and P1–P3 release gates. Existing query records remain the planned +carrier; this merge demonstrates no additional wire requirement. + +## Full configured language SQL matrix follow-up + +The initial broad integration run used Python only. The October 24 follow-up +runs the configured native SQL lanes across all seven SDKs on the same final +1.5 binary (`98842b7122ad5fb2f07c04f5d551bc87eb5103a961d948529e0c849088fe440e`). +All lanes meet their unchanged execution floor and declared-skip gate. + +| Worker | Passing SQL files | Expected skips | +| --- | ---: | ---: | +| Python | 356 | 33 | +| Go | 358 | 32 | +| Rust | 342 | 37 | +| TypeScript, each Arrow backend | 342 | 46 | +| Java | 336 | 37 | +| C# | 342 | 37 | +| C++ | 341 | 38 | + +TypeScript runs both ArrowJS and Flechette. Python/TypeScript/Java/C# each pass +2/2 separate subprocess crash tests; the other primary lanes include those files. +Counts are files, not assertions. Optional-service/extension/transport and absent +provider-fixture skips are not passes. Dedicated native references run separately, +and the C++ lane supplements the six targets in native `make test_languages`. +This does not establish a full HTTP matrix, every optional provider capability, +other platforms, or DuckDB 2 runtime support. + +Fixes include complete Go inline bind serialization; truthful immutable constraint +facts and historical snapshots; offered read-profile selection without write +claims; ordinary Go fixture nullability; owner-qualified native macro CTE/AT +expressions; and plain-table lookup under nondefault context without evaluating +unrelated generated columns. Native guards for actual expression use remain. +Duplicate relative/absolute SQL registrations no longer execute a file twice. +The hostile metadata workers now corrupt encoded get/list/bulk collections. +Bulk framing failures are asserted through the direct diagnostic, preserving +main's optional bulk-discovery fallback; semantic security failures still test +materialization from the bulk snapshot. + +Native contracts pass 12 cases/165 assertions; three column/reference files pass +252 assertions, with schema dependency and nullable-default probes also passing. +The loadable extension and CLI build and pass actual load/query checks. Full +changed SDK unit suites pass. No wire fields, generated schema/hash, or engine +pins change in this follow-up. + +Evidence: `logs/full-language-slt-20261024-75jtiyf_/matrix-summary.json`, per-lane +logs and CSVs, `source-checkpoint.json`, and `commits.json` in the owned EC2 workspace. +The corresponding manifest checkpoint retains exact limits and qualification data. +Earlier failed/interrupted/stale-worker runs remain diagnostic evidence, not +successful qualifications. The final breaking protocol version/routing choice +and P1–P3 release gates remain open before treating this as main/release-ready. diff --git a/docs/protocol/duckdb-2.0-manifest.json b/docs/protocol/duckdb-2.0-manifest.json new file mode 100644 index 00000000..0b4b785d --- /dev/null +++ b/docs/protocol/duckdb-2.0-manifest.json @@ -0,0 +1,12582 @@ +{ + "format_version": 1, + "objective": "Complete P0-P3 and M0-M4 of the VGI DuckDB 2.0 plan, preparing the combined protocol on the 1.5 engine before the separate native port.", + "status": "in_progress", + "version_decision": { + "status": "major_and_routing_selected_release_qualification_pending", + "published_protocol": "vgi.v2/2.2.0", + "breaking_candidates": [ + "Raw column paths replacing flat/dotted targets", + "Shared constraint definitions replacing index-based groups", + "Overload collections replacing single macro definitions" + ], + "publication_policy": "One coordinated public transition after qualification; no intermediate stable feature releases.", + "current_diff": "breaking: existing flat column targets become raw component lists; ADD/type change gain required targets; required_filters becomes raw three-level path groups; view metadata carries required aliases and security mode; view creation carries comments/tags/column comments and adds a view-column comment mutation; shared CatalogMetadataChanges and catalog_schema_alter_metadata add atomic schema comment/tag mutation; dependency identity/graph records and catalog_dependencies add complete transaction-visible graph discovery; negotiated query offer/selection, preparation/release RPCs, and typed parameter batch metadata join the combined successor transition; macro family records and recursive tuple/variant metadata profiles join the combined successor transition; required shared SQL/macro context and canonical constraint records join the successor schema inventory; required canonical table constraint lists replace the legacy parent arrays and revision-checked catalog_table_constraints_apply joins the public method surface; typed index definitions/options replace legacy index fields and add catalog_index_option_definitions; required semantic ALTER flags and raw subject/dependent column paths extend embedded dependency records; SET DEFAULT requires binary SqlExpression IPC and USING accepts nullable binary IPC; CREATE/ADD defaults use the canonical field codec, without new capability negotiation; CatalogAttachResult.default_schema becomes nullable UTF-8, preserving no implicit default independently of an explicit raw schema name; MacroInfo and MacroCreateRequest require nonempty overloads and remove legacy parent parameters/definition/parameter_default_values/arguments_schema; canonical families need no overload read/create opt-in; existing column comment/tags Arrow metadata gains strict shared validation and preservation on CREATE/ADD/discovery, with no record/RPC change; generated-column authoring/discovery/CREATE consistently use the existing canonical SqlExpression/category metadata, replacing legacy generated_expression strings without a new RPC or record layout; BindResponse adds optional projection_expression_profiles and table_function_projection adds structured early subset acceptance with canonical expressions and exact appended Arrow fields; BindResponse appends required repeatability enum UNKNOWN/STABLE_WITHIN_QUERY, with bounded native1.5 and SDK qualification; optional explicit transactional DML/DDL statement profile and fixed command/RETURNING output-schema metadata reuse existing query and transaction RPCs, with no new record fields", + "version_requirement": "The breaking successor uses major version 3.0.0 and routing name vgi.v3 across the extension and all seven SDKs. Published vgi.v2/2.2.0 remains incompatible. Exact major/minor matching remains in force; final coordinated candidate qualification and rollout proof are outstanding.", + "planned_diff": "Included before freeze: broader production-provider/adapter acceptance and release gates. Bounded scalar projection, bind-specific repeatability and Go/Python wrapped IPC qualification pass on the pinned1.5 build. Python RPC0.52.3 and Go RPC0.36.0 are merged, published and selected by the VGI dependency requirements/locks; registry/module installs are qualified without RPC source overrides. The successor identity is selected as vgi.v3/3.0.0; remaining transports, provider acceptance and coordinated candidate release gates still require qualification.", + "historical_additive_candidate": "vgi.v2/2.2.0", + "development_placeholder": "retired: vgi.v2/2.2.0 development placeholder replaced by unpublished vgi.v3/3.0.0", + "successor_protocol": "vgi.v3/3.0.0" + }, + "sources": { + "native_revision": "1a0a059", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "engine_20": "e1784fa4231072d7152b8aaa20977bdb8ff4ace3", + "duckdb_upstream": "c65ca930b9fe39b2e8d8ed8821018dca650965f4", + "audit_path": "docs/duckdb-2.0-catalog-audit.md", + "audit_sha256": "72f4fc9d8e63a0e98e6ab0caa6dc5d7ee2db1c50bfff6c6c9b94c9c568ba5139", + "plan_path": "docs/duckdb-2.0-plan.md", + "provenance": "Historical original field-audit pins. Current integration refs and qualification are in main_integration_checkpoint_2026_10_24; original audit identity/hash remains authoritative for field mappings." + }, + "shared_representations": { + "identity": "Raw components and attachment-relative CatalogObjectRef; no local aliases, pointers or OIDs. Full identity in all caches.", + "values": "TypedValueBatch preserving original Arrow metadata and typed NULLs. Option RESET differs from NULL and omission.", + "expressions": "Implemented: SqlExpression and SqlStatement with required QueryEvaluationContext, required matching macro context, canonical bounded base64-IPC column metadata. Finite scalar and durable CHECK proof on 1.5; general statement execution and new persistent DEFAULT/generated semantics remain unqualified.", + "capabilities": "Sealed attachment selection, current-provider checks and operation-specific semantic requirements. Read never implies mutation/enforcement. No generic ObjectRequirements record selected; column role/metadata profiles remain planned P1 work.", + "mutation": "Reuse existing begin/commit/rollback, write init/exchange/finalize and OLD/NEW changes. A failed write requires full transaction rollback and cannot partially commit. Do not automatically retry non-idempotent mutations after ambiguous dispatch; commit/rollback retain existing transaction-ID-based idempotency and retry semantics. Review BEGIN allocation/cleanup separately. New generic statement lifecycle, durable mutation replay/status and savepoint machinery are outside this package; audit concrete gaps before proposing wire additions.", + "rollout": "Selected: separate versioned endpoints/processes, draining old sessions/transactions, then reattach/reprepare. Retain exact major/minor matching. Do not migrate opaque tokens/transactions or automatically retry ambiguous mutations. Rollback needs old-compatible catalog state or a tested restore/migration. P3 deployment proof remains; no version-aware router selected.", + "column_paths": "Nine existing column RPCs use raw component paths, with per-operation nested DDL profiles and one-component 1.5 adapter targets. TableInfo.required_filters now uses AND-of-OR raw component paths across SDKs and the native 1.5 optimizer; native 2.0 integration remains open.", + "metadata_changes": "CatalogMetadataChanges: explicit comment presence plus literal string tag SET/REMOVE, atomic whole-patch validation, effect-sensitive versions/etags. Shared record; first negotiated operation is catalog_schema_alter_metadata.", + "dependencies": { + "records": [ + "CatalogObjectRef", + "ObjectDependency", + "DependencySnapshot" + ], + "method": "catalog_dependencies", + "authority": "Provider-resolved complete graph in one transaction snapshot; mutations revalidate and authorize atomically.", + "profiles": { + "read": "vgi.catalog.dependencies.read.v1", + "schema_ddl": "vgi.schema.lifecycle.v1" + }, + "status": "reference_implemented_full_provider_coverage_pending", + "semantic_alter": "Implemented required blocks_alter, nullable subject_column_paths/dependent_column_path, schema-aware validation, legal object-cycle handling and separate column/object DROP closures. Durable metadata reference supports generated columns/simple views; physical constraint/index/trigger integration remains pending." + } + }, + "areas": { + "capabilities": { + "title": "Catalog and client capabilities", + "disposition": "include", + "status": "contract_reviewed_existing_negotiation_with_planned_column_roles", + "records": [ + "CatalogCapabilities", + "ClientCapabilities", + "CatalogAttachResult" + ], + "existing_methods": [ + "catalog_attach" + ], + "new_methods": [], + "scope": [ + "Structured attachment capabilities, per-object restrictions where needed, client support for catalog/type semantics, and operation-specific unsupported errors", + "Distinguish read/materialize support, DDL support, data-write support, and transaction guarantees. A read-only worker may consume the same schema while advertising no mutations. An incapable client must reject a secure view, not ignore its security field." + ], + "capability_contract": "Keep CatalogCapabilities read/ddl/write/transaction intersections, category validation, prerequisites, sealed immutable selection and current-provider checks before effects. Absent offers grant no new profiles. Authoritative semantic fields (barrier/constraints/canonical expressions) must reject when unsupported; optional options/layout may project as documented with capability-scoped etags. Read facts never imply enforcement or mutation. Use the proposed explicit column role/metadata profiles below; no generic ObjectRequirements or new trigger-write carrier is approved.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "partial_reference_implementation", + "sources": [ + "vgi/catalog/capabilities.py", + "vgi/attach_capabilities.py", + "vgi/worker.py", + "tests/catalog/test_schema_options_protocol.py" + ], + "limits": "Attachment negotiation, current-profile checks and metadata projection implemented for schema options. Object-level secure-view/trigger restrictions and their execution checks remain pending." + }, + { + "status": "minimal_write_requirements_gap_audit_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership", + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes" + ], + "limits": "Current-definition checks must cover cached binds and writers predating trigger creation. Audit existing bind/init paths before selecting the minimum metadata carrier; no generic write-requirement, statement-ticket or receipt record is finalized by this review.", + "historical": true, + "superseded_by": "combined_protocol_review_2026_10_23" + }, + { + "status": "scope_and_field_mapping_reviewed_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "Source review only; remaining implementation and qualification tasks are explicit. No new runtime evidence." + } + ], + "implementation_work": [ + "Other SDK provider negotiation/execution enforcement needs qualification; generated records do not implement providers.", + "New column role/metadata profiles must be registered consistently; execution-role tests must cover absent/downgraded/wrong-category offers." + ] + }, + "schemas": { + "title": "Schema lifecycle and options", + "disposition": "include", + "status": "contract_reviewed_reference_qualified_native2_pending", + "records": [ + "SchemaInfo", + "CatalogSchemaCreateParams", + "OptionDefinitions", + "OptionChanges", + "OptionState", + "CatalogMetadataChanges" + ], + "existing_methods": [ + "catalog_schema_create", + "catalog_schema_get", + "catalog_schema_drop", + "catalog_schema_option_definitions", + "catalog_schema_alter_options", + "catalog_schema_alter_metadata" + ], + "new_methods": [], + "scope": [ + "Creation options, SET/RESET operations, option readback in `SchemaInfo`, and complete schema comment/tag mutations", + "Typed values, supported option names, expression policy, NULL versus absent versus RESET, nested targets, conflict policy, version changes, and rollback. Existing path/comment/tag create fields are reused." + ], + "capability_contract": "Existing schema.options.read/create/alter.v1, schema.metadata.alter.v1, schema.lifecycle.v1 and transaction.ddl.v1 define the scope. Options require exact typed declarations/allowlists and whole-candidate validation; lifecycle requires atomic RESTRICT REPLACE, authoritative CASCADE, snapshot versions and authorization. Metadata patches distinguish omission/NULL/empty and no-ops. Existing create/drop behavior does not itself claim the stronger lifecycle profile.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "shared_representation_implemented", + "sources": [ + "vgi/catalog/options.py", + "tests/catalog/test_options.py", + "docs/protocol/shared-catalog-values.md" + ], + "limits": "Common representation proof; subsequent RPC and lifecycle evidence is recorded separately below." + }, + { + "status": "schema_options_reference_implemented", + "sources": [ + "vgi/catalog/options.py", + "vgi/protocol.py", + "tests/catalog/schema_options_reference.py", + "tests/catalog/test_schema_options_protocol.py" + ], + "limits": "Python reference proves durable option creation/readback/alteration and transactional isolation/rollback/reconnect. Schema comment/tag alteration, dependency semantics and native adapter support remain pending." + }, + { + "status": "schema_creation_metadata_implemented", + "sources": [ + "docs/protocol/shared-catalog-values.md", + "tests/catalog/test_view_metadata_cross_sdk.py", + "tests/catalog/test_schema_metadata_protocol.py", + "native:test/cpp/test_schema_metadata.cpp" + ], + "limits": "Creation, discovery, ERROR/IGNORE and empty-schema REPLACE only. Schema comment/tag mutation, nonempty/descendant/dependency lifecycle and native 2.0 qualification remain open." + }, + { + "status": "schema_metadata_mutation_reference_implemented", + "sources": [ + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "vgi/worker.py", + "tests/catalog/schema_options_reference.py", + "tests/catalog/test_schema_metadata_protocol.py", + "tests/catalog/test_metadata_changes.py", + "tests/catalog/test_view_metadata_cross_sdk.py" + ], + "limits": "Python durable HTTP reference supplies qualified metadata mutation. Other SDKs have matching records/requests and explicit unsupported paths; production provider implementations and full SDK negotiation remain pending. Native schema-comment SQL is rejected by both inspected engine pins." + }, + { + "status": "dependency_aware_schema_lifecycle_reference_implemented", + "sources": [ + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "native:test/sql/duckdb2/schema_dependencies.test", + "native:src/storage/vgi_transaction.cpp" + ], + "limits": "Native 1.5 and durable Python reference qualification; production providers and native 2.0 remain open." + }, + { + "status": "scope_and_field_mapping_reviewed_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "Source review only; remaining implementation and qualification tasks are explicit. No new runtime evidence." + } + ], + "implementation_work": [ + "Production providers and native2 SQL/tree adapters; source status is stale where options, metadata mutation and dependency-aware lifecycle already have durable references." + ] + }, + "tables": { + "title": "Table creation and layout", + "disposition": "include", + "status": "contract_reviewed_reference_qualified_native2_pending", + "records": [ + "TableCreateRequest", + "TableInfo", + "TableLayout", + "SortKey", + "SqlExpression", + "OptionChanges", + "OptionState", + "TableLayoutChange", + "OptionDefinitions", + "TypedValueBatch" + ], + "existing_methods": [ + "catalog_table_create", + "catalog_table_get", + "catalog_table_option_definitions", + "catalog_table_alter_options", + "catalog_table_alter_layout" + ], + "new_methods": [], + "scope": [ + "Complete create metadata; table options and SET/RESET; partition and sort definitions on create, alter, and discovery", + "Preserve sort expression/direction/null order and distinguish desired layout from guarantees about scan output. Define whether alteration rewrites existing data, affects future writes, or rejects the change." + ], + "capability_contract": "Keep table.options.read/create/alter.v1, table.layout.read/create/alter_future/alter_rewrite.v1, table.metadata.create.v1 and transaction.ddl.v1. Layout mutation also requires the selected/current scalar expression profile for every retained/new key; layout never guarantees scan order. Canonical constraints keep their separate read/create/mutate/enforce profiles. CREATE validates all fields before replacement.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "shared_representation_implemented", + "sources": [ + "vgi/catalog/options.py", + "tests/catalog/test_options.py", + "docs/protocol/shared-catalog-values.md" + ], + "limits": "Catalog RPC integration, capability selection, option-definition discovery, durable mutation and rollback remain pending." + }, + { + "status": "partial_reference_implementation", + "sources": [ + "vgi/catalog/layout.py", + "vgi/worker.py", + "vgi/client/catalog_mixin.py", + "tests/catalog/table_layout_reference.py", + "tests/catalog/test_table_layout_protocol.py", + "conformance/catalog-contracts.json", + "docs/protocol/shared-catalog-values.md" + ], + "limits": "Python HTTP reference implements options and layout with actual fixture-row rewrites and durable DDL transactions. Expression semantics, production providers, native SQL enablement, full lifecycle review and coordinated release qualification remain pending." + }, + { + "status": "shared_expression_context_specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Required context and consistent scalar/statement roles are designed. Existing bounded layout/macro/column fixtures do not qualify these additional expression roles." + }, + { + "status": "scope_and_field_mapping_reviewed_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "Source review only; remaining implementation and qualification tasks are explicit. No new runtime evidence." + } + ], + "implementation_work": [ + "Native forwarding of create metadata/options/layout and native2 grammar; production layout rewrite and expression matrix.", + "Canonical constraint create/read wire is already implemented, not specified-only." + ] + }, + "columns": { + "title": "Columns and nested fields", + "disposition": "include", + "status": "canonical_columns_and_bounded_virtual_generation_implemented", + "records": [ + "ColumnPath", + "SqlExpression", + "RequiredFilterPaths" + ], + "existing_methods": [ + "catalog_table_column_add", + "catalog_table_column_drop", + "catalog_table_column_rename", + "catalog_table_column_type_change", + "catalog_table_column_default_set", + "catalog_table_column_default_drop", + "catalog_table_not_null_set", + "catalog_table_not_null_drop", + "catalog_table_column_comment_set" + ], + "new_methods": [], + "scope": [ + "Raw component `ColumnPath`; add/drop/rename/type/default/nullability operations; defaults, generated expressions, comments, tags, and supported compression policy", + "Consistent create/alter/read representation, USING expression semantics, generated dependencies, quoted dotted names, and complete validation before mutation. Keep existing Arrow metadata keys where sufficient." + ], + "capability_contract": "Canonical DEFAULT/USING, virtual generated definitions and Arrow comment/tags metadata are mandatory successor representations with no new opt-in. Existing nested-operation, transaction and constraint requirements remain. SqlExpression.profile identifies interpretation. Providers reject unsupported grammar/context and operations before effects. Stored generation and non-default compression are unsupported.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "shared_representation_implemented", + "sources": [ + "vgi/column_path.py", + "tests/test_column_path.py", + "conformance/catalog-contracts.json" + ], + "limits": "Original shared-record increment only; superseded for DDL targets by the raw-path increment below. Dotted required_filters still need operation/native migration." + }, + { + "status": "column_path_reference_implemented", + "sources": [ + "vgi/protocol.py", + "vgi/column_path.py", + "vgi/worker.py", + "tests/catalog/column_paths_reference.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_native_reference.py", + "docs/protocol/shared-catalog-values.md" + ], + "limits": "Nested row edits, parent NULL preservation, default/nullability changes, rollback/conflicts and profile guards are implemented in the durable reference. Shared expression qualification, generated columns, complete metadata/dependencies, required-filter migration, and native 2.0 bindings remain open." + }, + { + "status": "required_filter_paths_implemented_on_15", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/catalog/descriptors.py", + "vgi/column_path.py", + "tests/catalog/test_required_filters.py", + "tests/catalog/required_filters_native.py", + "conformance/catalog-contracts.json", + "docs/protocol/shared-catalog-values.md" + ], + "limits": "Native 1.5 validates full raw paths and preserves component prefixes through filters/struct extracts. Presence requirements are not a proof of authorization or remote predicate enforcement. Native 2.0 optimizer bindings and mutation/dependency semantics remain open." + }, + { + "status": "shared_expression_context_specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Required context and consistent scalar/statement roles are designed. Existing bounded layout/macro/column fixtures do not qualify these additional expression roles." + }, + { + "status": "scope_and_field_mapping_reviewed_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "Source review only; remaining implementation and qualification tasks are explicit. No new runtime evidence." + }, + { + "status": "canonical_column_payloads_implemented_on_15", + "sources": [ + "vgi/protocol.py", + "vgi/client/catalog_mixin.py", + "vgi/worker.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/test_column_expression_cli.py", + "native:src/vgi_column_expression.cpp", + "native:test/sql/duckdb2/column_paths.test", + "conformance/catalog-contracts.json" + ], + "checkpoint": "column_expression_checkpoint_2026_10_23", + "limits": "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context." + }, + { + "status": "descriptive_column_metadata_implemented", + "checkpoint": "column_metadata_checkpoint_2026_10_23", + "sources": [ + "python:vgi/catalog/column_metadata.py", + "python:vgi/catalog/catalog_interface.py", + "python:vgi/protocol.py", + "python:vgi/worker.py", + "native:src/vgi_column_metadata.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:src/vgi_catalog_api.cpp", + "native:test/cpp/test_column_metadata.cpp", + "native:test/run_column_metadata.py" + ], + "limits": "Native1.5 comments are materialized; tags and nested metadata remain authoritative Arrow annotations where its public column APIs lack support. Generated semantics are separate." + }, + { + "status": "virtual_generated_bounded_validation_passed", + "checkpoint": "virtual_generated_columns_checkpoint_2026_10_23", + "sources": [ + "vgi/catalog/sql.py", + "vgi/catalog/descriptors.py", + "vgi/worker.py", + "tests/catalog/generated_columns_reference.py", + "tests/catalog/test_generated_columns_protocol.py", + "native:src/vgi_column_expression.cpp", + "native:src/vgi_catalog_api.cpp", + "native:src/storage/vgi_table_entry.cpp", + "conformance/catalog-contracts.json" + ], + "limits": "Bounded deterministic top-level primitives/default context, single-worker native scans; no generated DML, stored/nested generation or native2 claim." + } + ], + "implementation_work": [ + "Broader generated grammar/types/contexts, native generated-table writes and provider acceptance remain qualification work; bounded CREATE/read uses existing carriers and dependency machinery.", + "Native1.5 parser rejects ALTER ADD generated columns. Stored generation is excluded; nested generation and physical-to-generated replacement have no qualified path.", + "Native2 public column-tag materialization and broader 2.0 grammar remain adapter-port work." + ] + }, + "views": { + "title": "Views", + "disposition": "include", + "status": "contract_reviewed_existing_wire_native2_qualification_pending", + "records": [ + "ViewInfo", + "CatalogViewCreateParams" + ], + "existing_methods": [ + "catalog_view_create", + "catalog_view_get", + "catalog_view_drop", + "catalog_view_rename", + "catalog_view_comment_set", + "catalog_view_column_comment_set" + ], + "new_methods": [], + "scope": [ + "Ordered aliases, security mode, create/read metadata, column comments and their mutation", + "Preserve the SELECT-definition contract or explicitly version its replacement. Security barriers must survive create/discovery/refresh/rebind; define name resolution, replacement, dependencies, and unsupported-reader behavior." + ], + "capability_contract": "Keep SELECT-only definition, raw owner identity, ordered aliases, explicit regular/barrier security and existing comment/tags/column_comments fields. Read barrier.v1 is independent of create barrier.v1; CREATE requires READ and transactional barrier effects need transaction.ddl.v1. Choose owner-relative engine late binding and derived output schema, not a persistent fixed-output or arbitrary-session-context promise. Unsupported security or query/owner semantics reject; no generic ViewCreateRequest or SqlStatement conversion is necessary.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "partial_protocol_implementation_on_15", + "implementation": "Ordered raw aliases in ViewInfo and catalog_view_create across all SDKs; native1.5 creation/materialization and cache reload coverage.", + "limits": "This alias-only checkpoint left security and creation metadata to subsequent increments. Later evidence records partial security/metadata lifecycle coverage; full capability and native2 qualification remain open." + }, + { + "status": "partial_protocol_implementation_on_15", + "implementation": "Creation comment/tags/column_comments and dedicated view-column comment mutation across all seven mutable SDK providers; native1.5 preserves empty comments, validates targets, normalizes aliases before the first SELECT and invalidates view cache.", + "sources": [ + "docs/protocol/view-metadata.md", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "tests/test_generated_cpp_request_builders.py" + ], + "limits": "Flat CREATE signature remains under combined contract review. Full capability negotiation, owner-relative query resolution, dependencies, fixed output contracts, concurrent production lifecycle semantics and native2 qualification remain open. Metadata-only providers do not bind SQL." + }, + { + "status": "scope_and_field_mapping_reviewed_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "Source review only; remaining implementation and qualification tasks are explicit. No new runtime evidence." + } + ], + "implementation_work": [ + "Native exactly-one-SELECT check; owner resolution/rebinding/concurrent lifecycle qualification.", + "Native2 security_type and optimizer enforcement plus capable provider negotiation in remaining SDKs." + ] + }, + "constraints": { + "title": "Constraints and indexes", + "disposition": "include", + "status": "canonical_constraints_and_typed_indexes_verified_on_15_bounded_adapters", + "records": [ + "ConstraintDefinition", + "ConstraintInfo", + "ConstraintSnapshot", + "ConstraintChange", + "ConstraintsApplyRequest", + "ForeignKeyDefinition", + "IndexKey", + "IndexDefinition", + "IndexInfo", + "IndexCreateRequest", + "TypedValueBatch", + "OptionDefinitions", + "OptionState" + ], + "existing_methods": [ + "catalog_index_create", + "catalog_index_get", + "catalog_index_drop", + "catalog_schema_contents_indexes", + "catalog_table_constraints_apply", + "catalog_index_option_definitions" + ], + "new_methods": [], + "scope": [ + "Constraint addition and supported mutation/discovery; full native integration of existing index RPCs; typed index options if string values are insufficient", + "Worker enforcement versus metadata-only declaration, existing-data validation, logical column identity, FK target paths, dependent objects, and atomic changes. Do not promise SQL constraint forms the selected engine itself rejects." + ], + "capability_contract": "Implemented read: vgi.constraint.records.read.v1, vgi.index.definition.read.v1; DDL: vgi.constraint.create.v1, vgi.constraint.mutate.v1, vgi.index.definition.create.v1, vgi.index.drop.v1; write: vgi.constraint.enforce.v1. DDL requires corresponding readback; UNIQUE requires enforcement, REPLACE also requires index drop, associated-index mutations require constraint mutation authority. Transaction DDL needs its existing profile. Additional FK match/action profiles remain unimplemented.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions", + "docs/protocol/shared-catalog-values.md#dependencies-for-semantic-alter", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Normative design/source review only. No canonical constraint/index record or provider enforcement implementation in this checkpoint." + }, + { + "status": "canonical_constraint_records_and_durable_reference_on_15", + "sources": [ + "vgi/catalog/constraints.py", + "vgi/catalog/dependencies.py", + "tests/catalog/constraints_reference.py", + "tests/catalog/test_constraints_reference.py", + "tests/catalog/test_constraint_contract.py", + "vgi/codegen/catalog_contract_fixtures.py", + "conformance/catalog-contracts.json" + ], + "limits": "Direct reference API only: top-level primitive columns, ENFORCED ALL_ROWS, immediate SIMPLE foreign keys with NO_ACTION/RESTRICT. No parent constraint-array replacement, public apply RPC, capability advertisement, index migration or native enforcement. Existing FK retargeting rejects. Seven-SDK codec and metadata evidence recorded in expression_constraint_checkpoint." + }, + { + "status": "typed_indexes_public_reference_and_native15_bounded", + "sources": [ + "vgi/catalog/indexes.py", + "vgi/protocol.py", + "vgi/worker.py", + "tests/catalog/index_protocol_reference.py", + "tests/catalog/test_index_public_contract.py", + "tests/catalog/test_index_public_lifecycle.py", + "tests/catalog/test_index_example_cross_sdk.py", + "conformance/catalog-contracts.json" + ], + "limits": "Native/reference regular and UNIQUE raw top-level keys with binary collation and explicit SQLITE.BTREE.V1. Rich typed options, expression/nested keys and PRIMARY association have wire coverage but no provider qualification. No constraint-backing closure, CASCADE, general ALTER dependency lifecycle or DuckDB2 runtime proof." + }, + { + "status": "reviewed_existing_checkpoint_retained", + "sources": [ + "constraint_public_checkpoint", + "index_public_checkpoint", + "semantic_alter_checkpoint", + "regression_checkpoint_2026_10_23" + ], + "limits": "Reference and native1.5 limits remain; physical provider/foreign-key closure and native2 qualification are not implied." + } + ], + "implementation_work": [ + "Qualify enforcement/mutation in additional production SDK providers before advertising support.", + "Extend beyond qualified top-level primitive immediate constraints only with new conformance evidence.", + "Qualify richer index algorithms/options, expression/nested keys, backing-constraint closure and native2 behavior." + ], + "review_decision": "Retain canonical constraints and typed index wire; use the existing bounded public/reference/native1.5 checkpoints. No additional constraint or index shape is selected in this source review." + }, + "macros": { + "title": "Macros \u2014 canonical families and bounded native 1.5 support", + "disposition": "include", + "status": "canonical_family_bounded_native15_implemented_and_validated", + "records": [ + "MacroInfo", + "MacroCreateRequest", + "MacroOverload", + "MacroParameter", + "SqlExpression" + ], + "existing_methods": [ + "catalog_macro_create", + "catalog_macro_get", + "catalog_macro_drop", + "catalog_schema_contents_macros" + ], + "new_methods": [], + "scope": [ + "Overload collections, declared parameter types, expression/default representation, documentation, and owner-relative definitions", + "Reconcile the existing single-definition request and typed-value defaults with engine overloads and expression defaults. Define resolution and stable round trips; do not introduce a function overload handle just to mirror DuckDB internals." + ], + "capability_contract": "One required complete overload family for both single and overloaded macros; no family-only read/create opt-in. Existing logical-type, dependency and transaction requirements still apply. SQL profile/context identifies interpretation, not permission.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "concrete_wire_contract_and_python_reference", + "paths": [ + "python:docs/protocol/macro-contract.md", + "python:vgi/catalog/macros.py", + "python:tests/catalog/test_macro_contract.py", + "python:tests/catalog/test_macro_overload_protocol.py" + ], + "limits": "Native family materialization/DDL and production owner binding remain unqualified; unsupported SDK adapters reject families.", + "historical": true, + "superseded_by": "macro_family_checkpoint_2026_10_23" + }, + { + "status": "shared_expression_context_specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Required context and consistent scalar/statement roles are designed. Existing bounded layout/macro/column fixtures do not qualify these additional expression roles.", + "historical": true, + "superseded_by": "macro_family_checkpoint_2026_10_23" + }, + { + "status": "required_evaluation_context_implemented", + "sources": [ + "vgi/catalog/evaluation_context.py", + "vgi/catalog/macros.py", + "vgi/catalog/sql.py", + "tests/catalog/test_expression_contract.py" + ], + "limits": "Required explicit context is generated across SDKs and default contexts must match their owner. Native macro-family integration and broader provider contexts remain pending.", + "historical": true, + "superseded_by": "macro_family_checkpoint_2026_10_23" + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:docs/protocol/macro-contract.md:20,56,81,121", + "python:vgi/catalog/macros.py:74,128,189,320", + "python:vgi/catalog/catalog_interface.py:648,791", + "python:tests/catalog/test_macro_contract.py:288,327,339", + "native:src/vgi_catalog_api.cpp:3003", + "native:src/storage/vgi_schema_entry.cpp:333" + ], + "limits": "Retains existing bounded runtime evidence; no new qualification.", + "historical": true, + "superseded_by": "macro_family_checkpoint_2026_10_23" + }, + { + "status": "canonical_family_implementation", + "checkpoint": "macro_family_checkpoint_2026_10_23", + "sources": [ + "python:vgi/catalog/catalog_interface.py", + "python:vgi/protocol.py", + "python:vgi/catalog/descriptors.py", + "python:vgi/worker.py", + "python:tests/catalog/test_macro_overload_protocol.py", + "native:src/vgi_macro_contract.cpp", + "native:src/storage/vgi_macro_set.cpp" + ], + "limits": "Bounded native1.5 discovery/execution; no native SQL CREATE, general dependency extraction or native2 qualification." + } + ], + "review_decision": "One canonical nonempty overload family is implemented across Python and generated SDKs. Simple authoring helpers normalize to one member. Native1.5 materializes complete scalar/table families for discovery/execution; native SQL CREATE MACRO remains unsupported. No new macro RPC, overload handle or capability flag.", + "implementation_work": [ + "Qualify broader owner contexts, nested native2 schemas, complete general body/type binding and dependency extraction, production provider transactions, and any separately authorized native SQL CREATE MACRO path. Finite durable reference and bounded native execution do not close those gates." + ] + }, + "dependencies": { + "title": "Dependencies and catalog consistency", + "disposition": "include", + "status": "contract_reviewed_reference_qualified_provider_native2_pending", + "records": [ + "CatalogObjectRef", + "ObjectDependency", + "DependencySnapshot", + "CatalogMetadataChanges", + "ConstraintInfo", + "ColumnPath" + ], + "existing_methods": [ + "catalog_version", + "catalog_contents", + "catalog_schema_drop", + "catalog_table_drop", + "catalog_view_drop", + "catalog_dependencies", + "catalog_table_rename", + "catalog_table_column_rename", + "catalog_table_column_type_change", + "catalog_table_column_drop" + ], + "new_methods": [], + "scope": [ + "Shared qualified object references; explicit ownership/drop/alter dependency semantics; mutation visibility and metadata-version rules", + "Settle worker versus engine authority, RESTRICT/CASCADE, rename effects, transaction-local reads, committed versions/etags, and retained entries. Include dependency records where enforcement across clients requires them; no DuckDB pointers or OIDs on the wire." + ], + "capability_contract": "Use existing catalog.dependencies.read.v1 for complete snapshot graph and schema.lifecycle.v1 for schema guarantees. The provider owns semantic rename/type/drop enforcement whether or not a client asks to read the graph; dependency-aware transactional mutation requires transaction.ddl.v1. Complete closure, schema validation and authorization occur atomically at mutation. Object owned edges are drop lifecycle only, never SQL principal ownership. Keep dedicated metadata RPCs; generic object_metadata_set/MetadataPatch/MetadataSnapshot are not selected.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "dependency_graph_and_schema_lifecycle_reference_implemented", + "sources": [ + "vgi/catalog/dependencies.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies.py", + "tests/catalog/test_dependencies_protocol.py", + "docs/protocol/shared-catalog-values.md" + ], + "limits": "Schema/table/simple-view authority and shared ownership planner only. Broader object-kind enforcement, rename policy, cross-attachment authority and production SDK providers remain pending." + }, + { + "status": "semantic_alter_and_column_dependencies_reference_implemented", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "tests/catalog/test_constraint_contract.py", + "conformance/catalog-contracts.json", + "native:test/run_semantic_alter.py", + "docs/protocol/shared-catalog-values.md#dependencies-for-semantic-alter" + ], + "limits": "Metadata-only durable reference: table/column rename, type/drop, generated-column DAG and simple-view CASCADE, transactions/reconnect. No row conversion, physical generated values, production constraint/index/trigger closure, SQL rewrite or native2 qualification. Native1.5 conversion expressions and canonical generated-column materialization reject." + }, + { + "status": "scope_and_field_mapping_reviewed_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "Source review only; remaining implementation and qualification tasks are explicit. No new runtime evidence." + } + ], + "implementation_work": [ + "Physical production constraint/index/FK closure and native2 cross-schema invalidation remain open.", + "SQL CHANGE OWNERSHIP explicitly deferred, not represented by ObjectDependency." + ] + }, + "transactions": { + "title": "Transactions and write execution", + "disposition": "existing_contract", + "status": "contract_reviewed_existing_lifecycle_native15_bounded_verified", + "records": [ + "CatalogCapabilities" + ], + "existing_methods": [ + "catalog_transaction_begin", + "catalog_transaction_commit", + "catalog_transaction_rollback" + ], + "new_methods": [], + "scope": [ + "Reuse existing transaction handles, begin/commit/rollback, write init/exchange/finalize and OLD/NEW results; audit concrete DuckDB2 gaps before adding wire fields", + "Keep supported DDL, DML and trigger effects in the existing provider transaction; failed writes cannot partially commit. No savepoints, durable replay/status service or general statement ticket/receipt framework in this package. No automatic retry of non-idempotent mutations after ambiguous dispatch; commit/rollback retain existing transaction-ID-based idempotency." + ], + "capability_contract": "Retain supports_transactions, existing attachment-bound transaction handles and BEGIN/COMMIT/ROLLBACK, init/input/finalize and result modes. transaction.ddl.v1 qualifies advertised DDL in that transaction; do not infer mixed/native2 forms solely from the flag. Accepted writes roll back the whole transaction on partial-effect failure. Keep idempotent completion retries and single-attempt BEGIN/non-idempotent dispatch. No statement coordinator, replay/status, savepoint, new mixed profile or trigger requirement carrier.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "date": "2026-10-15", + "status": "user_scope_decision_existing_lifecycle_gap_audit_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes", + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "This scope correction adds no runtime guarantee. Existing transaction, mutation no-retry and reference checkpoint evidence remains valid within its tested limits. Unsupported composite writes remain disabled until existing paths prove atomic completion and full rollback on failure." + }, + { + "date": "2026-10-15", + "status": "existing_write_path_source_audit_complete_runtime_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes", + "native:src/storage/vgi_transaction.cpp", + "native:src/storage/vgi_physical_write.cpp", + "native:src/vgi_function_connection.cpp", + "native:src/include/vgi_unary_retry.hpp" + ], + "limits": "Source-only findings and exact engine/worker anchors are in write_path_source_audit_2026_10_15. EC2 unavailable; no new runtime qualification, wire change or gate closure. Raw-drain and signed-HTTP token findings require reproduction." + }, + { + "date": "2026-10-22", + "status": "native15_transaction_exception_cleanup_verified", + "sources": [ + "native:src/storage/vgi_transaction.cpp", + "native:test/cpp/test_transaction_cleanup.cpp", + "native:test/run_transaction_cleanup_probe.py", + "tests/catalog/transaction_cleanup_probe.py" + ], + "limits": "Native local ownership/cache retirement only: 960 assertions and six subprocess callback traces pass after reproducing 24 lifetime failures before the fix. Existing transactional semantic ALTER regression passes. Diagnostic provider has no row mutations; application error after completion is not transport loss. Duplicate completion, BEGIN cleanup, signed HTTP and broader write atomicity remain separate." + }, + { + "status": "scope_and_field_mapping_reviewed_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "Source review only; remaining implementation and qualification tasks are explicit. No new runtime evidence." + } + ], + "implementation_work": [ + "Existing 17-case signed HTTP stored-data matrix already proves bounded native1.5 INSERT/CTAS failure rollback and earlier-write rollback; stale pending status must cite it.", + "Expanded native2 ALTER/multi-DML/OLD-NEW and deferred RETURNING remain qualification/adapter work; trigger implementation deferred.", + "Provider-owned orphan cleanup after lost BEGIN remains documented; no TTL reaper added." + ] + }, + "triggers": { + "title": "Triggers \u2014 reviewed deferral", + "disposition": "defer", + "status": "reviewed_deferred_existing_coverage_retained", + "records": [ + "CatalogObjectRef", + "ObjectDependency", + "SqlStatement" + ], + "existing_methods": [], + "new_methods": [], + "scope": [ + "Retain existing table-owned identity, SQL/context, dependency and OLD/NEW carriers; defer new persistent CRUD and execution profiles", + "Reopen for a concrete provider need with exclusive execution ownership, exact statement/row semantics, complete discovery and rollback proof. Later CRUD/capabilities require another coordinated SDK update." + ], + "capability_contract": "No new capability selected or advertised for this deferred feature family. Defer new persistent trigger CRUD and trigger execution profiles; retain existing table-owned identity, SQL/context, dependency, transactions, rows/changes and finalization carriers. A DuckDB2 port alone does not require a trigger protocol.", + "gates": [], + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_contract_gap_audit_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership", + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes", + "docs/protocol/shared-catalog-values.md#dependencies-for-semantic-alter" + ], + "limits": "Pinned grammar still establishes singular events and one-component UPDATE OF. Minimal persistent metadata, execution ownership and stale-bind protection remain necessary review subjects; prior generic statement ticket/receipt proposals are superseded. No trigger execution is qualified." + }, + { + "date": "2026-10-15", + "status": "engine_scheduling_and_existing_changes_carrier_source_audited", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership", + "native:src/include/storage/vgi_table_entry.hpp", + "native:src/storage/vgi_physical_write.cpp", + "vgi/write_results.py", + "vgi/_test_fixtures/writable/generic.py" + ], + "limits": "Start with ENGINE execution and provider-persisted definitions. Engine ScanTriggers currently conflates discovery/executable selection; backend-owned native discovery needs filtering. Statement expansion needs rows; UPDATE OLD capture needs existing changes adaptation and duplicate-target handling. No native2 execution proof." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Deliberate scope deferral, not implemented or qualified feature coverage." + } + ], + "record_inventory_status": "Only already-existing reusable carriers remain in records. Deferred candidate names are excluded from the included freeze inventory; they are not implemented APIs.", + "deferred_candidate_records": [ + "TriggerDefinition", + "TriggerInfo", + "TriggerCreateRequest" + ], + "deferred_candidate_methods": [ + "catalog_table_triggers", + "catalog_trigger_get", + "catalog_trigger_create", + "catalog_trigger_drop", + "catalog_trigger_alter_metadata" + ], + "reason": "The additional contract is justified specifically by VGI-managed persistent cross-client trigger definitions, not by backend triggers already enforced internally or by the existence of 2.0 table hooks. That product requirement is not established. Freezing metadata before proving exclusive execution ownership, exact OLD/NEW and statement behavior would advertise semantics that the current native path does not provide.", + "later_release_cost": "Trigger CRUD is approximately five new methods plus definition/info/create records and selected capabilities: additive in shape but a coordinated SDK/version redistribution under current matching. Existing CatalogObjectRef(kind=trigger,owner_table), SqlStatement, dependencies and OLD/NEW changes significantly reduce future change size. This review does not justify new general WriteRequirements, statement IDs, tickets, savepoints or recovery RPCs.", + "review_decision": "Defer new persistent trigger CRUD and trigger execution profiles; retain existing table-owned identity, SQL/context, dependency, transactions, rows/changes and finalization carriers. A DuckDB2 port alone does not require a trigger protocol.", + "business_cost_of_deferral": "No portable CREATE/list/get/drop/metadata API for remote trigger definitions, no engine-executed VGI triggers, and no cross-client trigger introspection. A backend may continue owning internal triggers as part of its actual write implementation, but per-batch SQL does not establish original SQL statement-trigger semantics and is not a new advertised VGI capability.", + "reopening_proof": [ + "A real user/provider need for persistent cross-client trigger management, then one durable CRUD/reference lifecycle with complete authorized snapshots and dependency closure.", + "Pinned-2.0 native ENGINE mode proof for a deliberately bounded statement trigger: order, zero input, multiple batches, exact rows/changes mapping and full transaction rollback after action/finalization failure.", + "Exclusive ENGINE versus BACKEND ownership, incapable writer rejection and current definition checks through existing bind/transaction mechanisms; do not infer a new wire field from hypothetical stale prepared plans.", + "Keep pinned unsupported grammar rejected. BACKEND definitions cannot be installed as executable native entries without addressing ScanTriggers conflating discovery/execution." + ], + "deferred_implementation_work": [ + "Implement provider-persisted trigger CRUD/readback and native Create/Scan/Get/DropTrigger hooks for ENGINE execution, preserving case-insensitive alphabetical order; qualify BACKEND discovery/execution separately.", + "Adapt existing rows/changes to pinned statement-trigger RETURNING and UPDATE NEW-then-OLD output; honor duplicate-target policy and mode-aware finalization without a new OLD/NEW wire record.", + "Determine the minimum table-owned trigger metadata, dependency and current-definition checks needed to select exactly one execution owner and reject stale/incapable writes before effects.", + "Qualify supported BEFORE/base/AFTER ordering, row/statement firing, transition data and full transaction rollback using the existing write path; leave unsupported forms disabled." + ] + }, + "query": { + "title": "Catalog query execution \u2014 explicit 1.5 adapter implemented; automatic pending", + "disposition": "include", + "status": "reference_and_native_15_explicit_implemented_automatic_pending", + "records": [ + "ClientQueryCapabilities", + "QueryCapabilities", + "QueryEvaluationContext", + "QueryObjectReference", + "CatalogQueryPrepareRequest", + "CatalogQueryPrepareResult", + "QueryParameters" + ], + "existing_methods": [ + "bind", + "init", + "catalog_query_prepare", + "catalog_query_release" + ], + "new_methods": [], + "scope": [ + "Negotiated query preparation, typed parameters/context, prepared-result schema and lifetime, and execution through the existing TABLE stream", + "Reuse the existing read-query proposal. Separate explicit provider-dialect queries from automatic semantic-equivalence pushdown; define decline-before-execution, transaction/identity binding, ordered results, cancellation, token cleanup, and reconnect behavior." + ], + "capability_contract": "ClientQueryCapabilities/QueryCapabilities select duckdb.sql.v1 with vgi.remote.readonly.v1 for explicit mode. vgi.duckdb.relational.v1 automatic mode requires transaction support and separately certified engine/context equivalence; it is not currently advertised by the finite reference. Worker opt-in is the ATTACH selection of automatic mode/profile from the client offer. The qualified 2.0 adapter uses discovered automatic support by default; absent or explicit-only support stays local without preparation RPCs. Capability discovery is the sole enablement mechanism; there is no user/session pushdown control and no new wire field. Per-query eligibility and preparation acceptance still apply.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + "docs/protocol/shared-catalog-values.md#prepared-read-queries-and-typed-parameters", + "vgi/catalog/query.py", + "vgi/query_parameters.py", + "vgi/query_execution.py", + "vgi/query_provider.py", + "tests/catalog/query_reference.py", + "tests/catalog/test_query_protocol.py", + "conformance/catalog-contracts.json", + { + "status": "native_15_explicit_reference_validated", + "source": "native_query_adapter_validation", + "sources": [ + "native:src/vgi_query_function.cpp", + "native:test/run_queries.py", + "tests/catalog/query_native.py" + ], + "limits": "18 finite-reference subprocess scenarios. No general provider or automatic native 2.0 qualification." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/query.py:104,122,182,200,252,281", + "python:vgi/query_parameters.py:40,55", + "native:src/vgi_query_function.cpp:241,255,272,304,317", + "haybarn:src/include/duckdb/catalog/catalog.hpp:90,417" + ], + "limits": "Retains existing bounded runtime evidence; no new qualification." + }, + { + "status": "automatic_planning_source_review_2026_10_24", + "source": "automatic_query_pushdown_plan_2026_10_24", + "sources": [ + "docs/protocol/vgi-catalog-query-pushdown-design.md#implementation-checkpoint--october-24-2026", + "native:src/vgi_query_rpc.cpp", + "native:src/vgi_query_function.cpp", + "tests/catalog/query_reference.py", + "haybarn:src/include/duckdb/planner/logical_plan_sql_exporter.hpp", + "haybarn:src/planner/sql_export/bound_expression_sql_exporter.cpp" + ], + "limits": "Planning/source evidence only. No native 2.0 build, automatic replacement, general provider or mutable snapshot qualification. Engine API evidence uses exact e1784fa42310, not later Haybarn HEAD." + }, + { + "status": "native2_initial_compile_inventory_2026_10_24", + "sources": [ + "native:docs/duckdb-2.0-a0-diagnostics.md", + "native:docs/duckdb-2.0-a0-diagnostics.json", + "native:scripts/duckdb2_diagnostics.py" + ], + "limits": "Configuration passes;53 of 107 native C++ sources compile,54 fail, none unattempted. Generated describe.c also compiles. Query RPC builder compiles but executor remains blocked by shared catalog declarations. No link/runtime/automatic qualification." + } + ], + "implementation_notes": [ + "Parameter mapping is schema metadata on one Arrow batch; preparation carries schema-only IPC and execution carries one same-schema row. No JSON type list or value interpolation.", + "Prepared tokens and execution locators are opaque; preparation release is a wire RPC. Resolve/start/read/close remain provider hooks over the existing TABLE stream.", + "The finite Python reference accepts six exact read queries; explicit support never implies automatic semantic equivalence.", + "Native 1.5 explicit vgi_query is implemented and tested: vgi_query_enabled before ATTACH, typed parameters, fresh prepared-statement binding, exact output validation and independent cleanup. See native_query_adapter_validation.", + "General SQL providers, shared mutable backend snapshots, active-query cancellation, bounded incremental spooling and independent abandoned-token cleanup remain open. Existing signed-HTTP fragment continuation is qualified separately; automatic native 2.0 and wider HTTP/WASM query coverage remain open." + ], + "review_decision": "Keep existing negotiated prepare/release plus TABLE bind/init/stream contract; no additional query RPC family or parameter value record required.", + "implementation_work": [ + "A0/A1: isolated pinned 2.0 diagnostic build and basic catalog/scan/explicit-query port. Keep the 1.5 lane unchanged; mechanical compatibility of the monolithic source list or explicit module exclusions is required even for a read-only target.", + "A2: bounded DuckDB-backed SQL provider serving ordinary logical scans and remote queries under the same existing backend transaction/snapshot, with active-query interruption and incremental result bounds. Metadata etags and per-bind repeatability do not prove shared data visibility.", + "A3/A4: early bound-plan eligibility/export before scalar projection and multi-scan transforms; native automatic capabilities, context digest, object/output manifest and verified positional replacement. Reuse preparation ownership and the conservative TABLE executor; preserve the original local plan only on unsupported shape or valid decline.", + "A5: bounded semantic/snapshot/authorization/lifetime/transport and actual plan-copy/prepared-freshness qualification, with EXPLAIN and result-traffic evidence. No local replay after remote errors or execution. Qualify worker opt-in through attachment discovery and automatic behavior for selected compatible support, absent/explicit-only capabilities and per-query decline. No user/session pushdown control.", + "Follow-up: typed automatic parameter export. Audited bound expression exporter rejects BOUND_PARAMETER; first milestone uses the existing zero-parameter representation and leaves parameterized candidates local. No new wire parameter design.", + "See automatic_query_pushdown_plan_2026_10_24 for checkpoints, dependencies and provisional effort. Keep EXECUTE_STATEMENT and CONNECT unsupported. No demonstrated additional wire datum or query RPC family is required; full M0-M4 and release gates remain open." + ] + }, + "logical_types": { + "title": "Logical type preservation \u2014 concrete contract and codecs implemented; adapters pending", + "disposition": "include", + "status": "reference_implemented_native_qualification_pending", + "records": [ + "TypedValueBatch" + ], + "existing_methods": [ + "bind", + "init" + ], + "new_methods": [], + "scope": [ + "Recursive VGI TUPLE annotation and canonical Arrow Parquet Variant capability with unshredded metadata/value storage", + "Qualify canonical Variant semantics, missing versus NULL and SDK byte/metadata preservation at each engine pin. Original DuckDB integer width/signedness and its full type domain are not promised; unsupported mappings reject. Reject opaque aggregate-state layouts without a portable contract." + ], + "capability_contract": "CatalogCapabilities.read selects vgi.type.tuple.v1 and vgi.type.variant.parquet.v1 independently. TUPLE retains vgi.logical_type.v1=tuple; VARIANT uses canonical ARROW:extension:name=arrow.parquet.variant with empty extension metadata and unshredded metadata/value struct storage. The former per-cell IPC profile and custom scalar encodings are superseded and rejected.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "historical_checkpoint_variant_encoding_superseded", + "paths": [ + "python:docs/protocol/logical-types.md", + "python:vgi/logical_types.py", + "python:tests/test_logical_types.py", + "native:test/cpp/test_schema_metadata.cpp" + ], + "limits": "Original types/macros checkpoint used the now-superseded per-cell IPC VARIANT proposal. Its counts and revisions remain historical evidence only. TUPLE and macro contracts remain; canonical VARIANT requires separate validation." + }, + { + "status": "canonical_variant_transport_validated_native_adapters_pending", + "paths": [ + "python:docs/protocol/logical-types.md", + "python:vgi/logical_types.py", + "python:tests/test_logical_types.py", + "native:test/cpp/test_schema_metadata.cpp" + ], + "limits": "PyArrow 25.0.1 transports canonical struct/binary bytes opaquely; no public Variant API or general binary parser is claimed. Native 1.5 rejects unsupported canonical VARIANT. Backend decoding, native 2.0 mapping, shredding and full domain/performance qualification remain open." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:docs/protocol/logical-types.md:12,32,52,94,107,118,133", + "python:vgi/logical_types.py", + "python:tests/test_logical_types.py", + "native:test/cpp/test_schema_metadata.cpp", + "haybarn:src/common/arrow/arrow_type_extension.cpp:529,607", + "haybarn:src/common/arrow/arrow_converter.cpp:66", + "haybarn:src/common/types/variant/variant_iterator.cpp:RowIsValid", + "haybarn:src/common/types/variant/parquet_variant_encoding.cpp:391,615" + ], + "limits": "Retains existing bounded runtime evidence; no new qualification." + } + ], + "review_decision": "No additional wire design justified beyond the implemented negotiated TUPLE annotation and canonical unshredded Arrow Parquet VARIANT profile. Reject opaque aggregate states absent a portable, qualified engine contract; no placeholder descriptor.", + "implementation_work": [ + "Qualify actual native2 tuple import/export and canonical Variant engine mapping, binary decoding, missing versus NULL and original logical-domain limits. Python opaque transport validation does not qualify semantic execution; native1.5 rejects these profiles. Upstream root Variant null can collapse to SQL NULL and not every DuckDB type maps canonically. Preserve explicit mapping limits; do not invent width/signedness side channels to simulate a promise the canonical format does not make." + ] + }, + "optimizer": { + "title": "Optimizer and observability additions \u2014 P1 design required", + "disposition": "include", + "status": "bounded_projection_repeatability_native15_sdk_qualified_release_pending", + "records": [ + "FunctionInfo", + "BindResponse", + "TableFunctionPlanRequest", + "InitRequest", + "TableFunctionDynamicToStringResponse", + "TableFunctionProjectionRequest", + "TableFunctionProjectionResponse" + ], + "existing_methods": [ + "table_function_plan", + "table_function_cardinality", + "table_function_statistics", + "table_function_dynamic_to_string" + ], + "new_methods": [ + "table_function_projection" + ], + "scope": [ + "Include scalar projection/result mapping and bind-specific repeatability; reuse diagnostics, split selection and parallelism; defer new typed metrics", + "Prove support decisions before optimizer replacement, exact result types/slots, local residuals and query-scoped repeatability. Filter v2 currently describes Boolean predicates. Existing monotonicity/runtime filters are retained; no speculative metrics or partition-ID fields." + ], + "capability_contract": "BindResponse.projection_expression_profiles defaults empty independently of existing column-selection projection_pushdown. table_function_projection is pure pre-optimizer negotiation; empty accepted_indices/null bind_result declines; a validated subset returns a complete replacement bind result with unchanged source prefix and exact appended fields. Unsupported expressions stay local. Repeatability remains a separate P1 contract/proof; neither acceptance nor result caching grants retry permission.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "source_review_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "P0 scope mapping only; projection and repeatability design/proof remain explicit P1 blockers." + }, + { + "status": "bounded_projection_checkpoint_2026_10_23", + "source": "docs/protocol/projection-expressions.md", + "checkpoint": "projection_expressions_2026_10_23", + "limits": "Repeatability, broader projection shapes/grammar/cache/split identity, Go outer framing qualification, production-provider/release gates and native2 port remain open." + }, + { + "status": "bounded_repeatability_checkpoint_2026_10_24", + "source": "docs/protocol/scan-repeatability.md", + "checkpoint": "scan_repeatability_and_go_framing_2026_10_24", + "limits": "Finite native1.5/SDK/provider proofs only; production state lifetime, Python outer IPC framing, all transports and native2 integration remain open." + }, + { + "status": "bounded_python_rpc_framing_checkpoint_2026_10_27", + "source": "docs/protocol/projection-expressions.md", + "checkpoint": "python_rpc_framing_checkpoint_2026_10_27", + "limits": "Single-value IPC and public projection requests qualified over subprocess and localhost HTTP. Publish/pin the tested Python and Go RPC dependencies; broader transports, provider lifetime and native2 integration remain open." + }, + { + "status": "published_rpc_dependency_checkpoint_2026_10_27", + "checkpoint": "rpc_publication_checkpoint_2026_10_27", + "source": "docs/protocol/projection-expressions.md", + "limits": "RPC fixes released and VGI dependencies pinned; source overrides are no longer required. Final VGI protocol identity, remaining provider/transport qualification and native2 integration remain open." + } + ], + "proposed_records": [], + "record_inventory_status": "Canonical SqlExpression payloads, bind-specific profile offers and the explicit TableFunctionProjectionRequest/Response replace the provisional ProjectionExpression. Append-only result slots use ordered accepted_indices; no separate ResultMapping or preparation-ticket record. BindResponse also carries one repeatability enum; existing opaque state anchors the promise.", + "implementation_work": [ + "Bounded proposal/partial-decline/result-schema and native1.5 optimizer proof passed; qualify broader production providers and transport boundaries before freeze.", + "Broaden scalar grammar only with error-order, context, NULL, overflow and logical-type equivalence; throwing expressions stay local.", + "Qualify direct function calls, generated/nested/rowid/delegated/partitioned scans and filter/runtime-hint coordinates before enabling them.", + "Qualify expanded-schema cache/split identity before enabling those paths; disabled accepted paths must not silently reactivate.", + "Native DuckDB2 optimizer API integration remains separate from the pinned1.5 proof.", + "Bounded repeatability proof passed on native1.5 and SDK references; qualify shipping provider state lifetime and native2 sampling/dynamic-filter/scan-classifier behavior.", + "Go and Python wrapped IPC fixes are published/pinned as RPC-Go0.36.0 and PythonRPC0.52.3; published-dependency qualification is recorded in rpc_publication_checkpoint_2026_10_27." + ], + "review_decisions": { + "recommended_scope": "Retain projection-expression and repeatability design in the combined package; mark P0 scope reviewed and P1 contract/implementation proof outstanding. Reuse existing transport for metrics, partition selection and parallelism unless a concrete missing datum is demonstrated.", + "projection": { + "existing": "FunctionInfo.projection_pushdown and InitRequest/TableFunctionPlanRequest.projection_ids only select existing columns. InitRequest.output_schema is already an exact execution output contract. Filter v2 provides bounded expression grammar, typed literal/type payloads and evaluation context, but its root is BOOLEAN and all current Call nodes are typed BOOLEAN.", + "justified_new_information": "Implemented TableFunctionProjectionRequest/Response and per-bind projection_expression_profiles; canonical SqlExpression records parallel an exact Arrow result_schema; accepted_indices map appended output fields without source renumbering.", + "minimal_candidate": "Selected append-only subset acceptance before optimizer replacement. Native1.5 first proof targets total INTEGER-to-BIGINT casts over ordinary non-generated catalog tables, preserving local expressions/filters. Accepted bind bytes and output schema flow through existing init; no execution resources or release RPC. Direct function calls and broader expression/scan shapes remain unqualified.", + "lifecycle_gap": "Addressed by pure table_function_projection after ordinary bind and before any local expression is replaced. Existing table_function_plan remains execution-time split enumeration. Native accepted plans require copy/serialization preservation and exclude cache/splits/hints not qualified for the expanded schema.", + "remaining_P1_questions": [ + "Bounded proposal/partial-decline/result-schema and native1.5 optimizer proof passed; qualify broader production providers and transport boundaries before freeze.", + "Broaden scalar grammar only with error-order, context, NULL, overflow and logical-type equivalence; throwing expressions stay local.", + "Qualify direct function calls, generated/nested/rowid/delegated/partitioned scans and filter/runtime-hint coordinates before enabling them.", + "Qualify expanded-schema cache/split identity before enabling those paths; disabled accepted paths must not silently reactivate.", + "Native DuckDB2 optimizer API integration remains separate from the pinned1.5 proof." + ], + "evidence": [ + "python:vgi/protocol.py:275,319,507,553", + "python:vgi/filter_v2.py:755,761,930,944,1001", + "native:src/vgi_table_function_impl.cpp:2462,2468,2515", + "haybarn:src/include/duckdb/function/table_function.hpp:215,408,523", + "haybarn:src/optimizer/type_pushdown.cpp:90,197,229,241,262" + ] + }, + "repeatability": { + "existing": "Scalar stability, cache TTL/transaction cache scope, replayable split tokens and scan execution_id do not assert query-scoped table repeatability. DuckDB2 consults the table-function callback only after its table-backed scan shortcut; that shortcut remains native-port work.", + "justified_new_information": "One required BindResponse.repeatability dictionary field: UNKNOWN or STABLE_WITHIN_QUERY. SDK defaults UNKNOWN, with no capability gate. The stable promise covers equivalent scans of one complete bind state within a statement.", + "minimal_candidate": "Selected and implemented in source: existing opaque bind/transaction state anchors the promise. Projection preserves it exactly. Stable prepared executions bind afresh, stale stable inline metadata triggers fresh bind, result caches lacking snapshot identity are bypassed, copies preserve state and serialized changed-state restores fail. No new coordinator, public snapshot token, release RPC or retry permission.", + "remaining_P1_questions": [ + "Native2 port must address table-backed LogicalGet repeatability classification before callback dispatch, plus sampling/dynamic-filter checks.", + "Qualify shipping provider state lifetime and any cache/split integration before relaxing the conservative initial adapter exclusions." + ], + "evidence": [ + "python:vgi/invocation.py:52,76,84,125", + "python:vgi/protocol.py:203,292,601,609", + "python:vgi/cache_control.py:75,79,88", + "haybarn:src/include/duckdb/function/table_function.hpp:533", + "haybarn:src/planner/logical_operator_repeatability.cpp:60,62,68" + ], + "specification": "docs/protocol/scan-repeatability.md", + "qualified_checkpoint": "scan_repeatability_and_go_framing_2026_10_24" + }, + "metrics": { + "decision": "Existing contract plus engine port; no TypedMetric wire record justified for baseline 2.0 coverage. Retain existing string diagnostics and locally measured native counters/timings.", + "mapping": "table_function_dynamic_to_string already returns ordered keys/values scoped by global_execution_id. Map these directly to OperatorMetrics.AddExtraInfo/SetExtraInfo. Native row/byte/time counters are local observations. Do not parse arbitrary diagnostic strings into typed counters.", + "remaining": "A future typed remote-metric feature must first name a needed remote measurement and define units, aggregation, scope, sample/reset lifetime and partial/cancel behavior. Existing per-thread RPC carries a query-wide ID, so blindly summing its returned totals can double-count; this is an adapter aggregation question, not proof a new generic metric bag is necessary.", + "evidence": [ + "python:vgi/protocol.py:663,678,688", + "haybarn:src/include/duckdb/main/profiler/profiling_node.hpp:16,24,46,49,56", + "haybarn:src/include/duckdb/function/table_function.hpp:503" + ], + "scope_disposition": "defer typed metrics; retain existing-contract engine mapping and explicit future feature cost without placeholder fields", + "later_release_cost": "A future typed metric envelope is normally additive but requires coordinated SDK redistribution under exact major/minor matching; units, aggregation and lifetime must be specified first." + }, + "partition_selection": { + "decision": "Existing contract plus engine port. Build a stable local mapping from the get_partition_stats vector to existing plan/split tokens; redeem only selected splits through InitRequest.split_tokens. No new selected_partition_ids field for this path.", + "remaining": "Partition statistics order must be stable for the pinned plan and selection must name the same data/version. Paginated or independently replanned partition inventories cannot be matched by an incidental ordinal. A new wire selection identity would be justified only for a provider whose selection cannot be expressed by choosing existing tokens; none is demonstrated here.", + "evidence": [ + "python:vgi/protocol.py:281,449,580,609,653", + "haybarn:src/include/duckdb/function/table_function.hpp:537,545" + ] + }, + "parallelism": { + "decision": "Existing contract plus engine port; no explicit parallelism enum demonstrated necessary. Native adapter owns worker scheduling using existing max_workers, splits, order preservation, batch indices and source/sink ordering. Map ordinary coordinated scans to self-managed parallelism and enforce existing caps.", + "evidence": [ + "python:vgi/metadata.py:516,538,596", + "python:vgi/invocation.py:144", + "python:vgi/protocol.py:604,643", + "haybarn:src/include/duckdb/function/table_function.hpp:572,580" + ] + }, + "runtime_filters": { + "decision": "Existing algorithm capability and root-only advisory artifact envelope retained; no additional envelope field selected.", + "evidence": [ + "vgi/metadata.py:150", + "vgi/metadata.py:594", + "vgi/filter_v2.py:977", + "docs/protocol/vgi-runtime-filter-artifacts.md" + ], + "limits": "Bloom/prefix-range candidate identities are not qualified exporters/evaluators. Unknown algorithms reject and known unsupported advisory roots are ignored while exact local residuals remain. Keep candidates unadvertised until immutable artifact semantics and no-false-negative tests pass." + } + } + }, + "sequences_types": { + "title": "Sequences and user-defined types", + "disposition": "defer", + "status": "reviewed_deferred_existing_coverage_retained", + "records": [ + "CatalogObjectRef", + "TypedValueBatch" + ], + "existing_methods": [], + "new_methods": [], + "scope": [ + "Define discovery/create/alter/drop, sequence allocation/concurrency/rollback rules, and portable enum/type definitions. Separate transferable metadata from engine constructor callbacks." + ], + "capability_contract": "No new capability selected or advertised for this deferred feature family. Defer remote sequence lifecycle/allocation and named-type catalog CRUD; keep existing supported resolved Arrow type transport. Split these two different features in the review record.", + "gates": [], + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:src/storage/vgi_schema_entry.cpp:408-432 rejects sequence, type and collation CREATE.", + "engine@pin:src/include/duckdb/catalog/catalog_entry/sequence_catalog_entry.hpp:62-74 nonvirtual operations and private state.", + "engine@pin:src/function/scalar/sequence/nextval.cpp:72-81 obtains DuckTransaction for the sequence catalog.", + "engine:src/include/duckdb/parser/parsed_data/create_type_info.hpp:19-36 type/query/constructors.", + "engine:src/planner/binder/statement/bind_create.cpp:970-1010 distinguishes resolved type and query-derived enum.", + "native:src/vgi_arrow_ipc.cpp:527-532 resolves dictionary type to its value type; Arrow dictionary encoding is not an enum domain contract.", + "python:vgi/catalog/dependencies.py:80-105 already accepts sequence/type identity kinds." + ], + "limits": "Deliberate scope deferral, not implemented or qualified feature coverage." + } + ], + "deferred_candidate_records": [ + "SequenceInfo", + "SequenceAllocation", + "TypeInfo", + "TypeCreateRequest" + ], + "deferred_candidate_methods": [ + "catalog_sequences", + "catalog_sequence_get", + "catalog_sequence_create", + "catalog_sequence_drop", + "catalog_sequence_allocate", + "catalog_types", + "catalog_type_get", + "catalog_type_create", + "catalog_type_drop" + ], + "reason": "Neither is required merely to port the 1.5 VGI behavior to 2.0. Sequence semantics require allocation authority and a genuine engine extension point, not discovery fields. Named-type identity/ENUM domain is a real schema-fidelity feature, but resolved Arrow storage is the currently supported interoperability boundary; the 2.0 constructor callback addition is not transferable metadata.", + "later_release_cost": "Sequence list/get/create/drop/allocate and type list/get/create/drop would normally be additive methods, but require another coordinated SDK/client/worker update under exact version matching. Type identity/domain annotations must be negotiated; redefining existing unannotated dictionary/string semantics would be breaking and should be avoided.", + "record_inventory_status": "Only already-existing reusable carriers remain in records. Deferred candidate names are excluded from the included freeze inventory; they are not implemented APIs.", + "review_decision": "October25 user explicitly retained named-type and ENUM deferral. Remote sequence lifecycle/allocation remains deferred; keep existing supported resolved Arrow transport.", + "business_cost_of_deferral": "Clients cannot use a remote catalog sequence as normal nextval/currval or allocate keys uniformly across clients. Schema migration/ORM tooling cannot discover/recreate named aliases/ENUMs, and CAST to a remote named type is unavailable. Ordinary supported primitive/nested Arrow value exchange continues; this must not be described as named-type or ENUM semantic coverage.", + "reopening_proof": [ + "Sequence: real custom-catalog nextval adapter against pinned engine; two concurrent clients produce no duplicates; explicit rollback/gap policy, overflow/cycle/restart behavior, and ambiguous allocation failure without retry.", + "Types: alias/ENUM create/get/list/drop/reconnect on durable provider; preserve exact identifier, enum label order and full domain including labels absent from batches; reject invalid values; round trip table columns, CAST and prepared results through all SDKs; prove no silent alias-to-string semantic claim.", + "Resolve dependencies, replacement and rename behavior before freezing a named-type carrier; demonstrate nested nullable fields and unknown-profile rejection on 1.5." + ] + }, + "collations_coordinates": { + "title": "Collations and coordinate systems", + "disposition": "defer", + "status": "reviewed_deferred_existing_coverage_retained", + "records": [ + "QueryEvaluationContext" + ], + "existing_methods": [], + "new_methods": [], + "scope": [ + "Determine which descriptions can be shared and which execution semantics require engine-local implementation; qualify equality/order behavior before advertising a collation." + ], + "capability_contract": "No new capability selected or advertised for this deferred feature family. Defer virtual collation/coordinate-system catalog discovery and CRUD. Collation implementation/certification is engine-only; retain the existing evaluation-context and Arrow metadata carriers where qualified.", + "gates": [], + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "engine:src/include/duckdb/parser/parsed_data/create_collation_info.hpp:28-35 contains ScalarFunction and semantic flags.", + "engine:src/include/duckdb/parser/parsed_data/create_coordinate_system_info.hpp:29-39 contains portable authority/code/PROJJSON/WKT strings; structure also exists on the 1.5 baseline.", + "engine:src/function/table/system/duckdb_coordinate_systems.cpp:59-66 enumerates actual COORDINATE_SYSTEM_ENTRY objects.", + "python:vgi/catalog/query.py:129-155 carries an explicit QueryEvaluationContext; docs/protocol/vgi-catalog-query-pushdown-design.md:325-335 requires certification beyond binary/UTC.", + "python:vgi/catalog/dependencies.py:80-95 has no collation or coordinate-system identity kind." + ], + "limits": "Deliberate scope deferral, not implemented or qualified feature coverage." + } + ], + "deferred_candidate_records": [ + "CollationInfo", + "CoordinateSystemInfo" + ], + "deferred_candidate_methods": [ + "catalog_collations", + "catalog_coordinate_systems", + "catalog_coordinate_system_get" + ], + "reason": "A collation is executable equality/order behavior, not a name plus flags. CRS descriptions are portable and comparatively simple, but new catalog CRUD is not required to preserve an existing spatial Arrow field's metadata. No demonstrated catalog-level consumer presently justifies another object family in this release.", + "later_release_cost": "Catalog discovery/CRUD adds methods and coordinate-system object identity kind (currently absent from CatalogObjectRef), so it costs another coordinated version/SDK pass. Certifying a collation already represented by QueryEvaluationContext may be a named-profile implementation extension without changing fields; executable remote collation callbacks need a separate qualified execution design.", + "record_inventory_status": "Only already-existing reusable carriers remain in records. Deferred candidate names are excluded from the included freeze inventory; they are not implemented APIs.", + "review_decision": "Defer virtual collation/coordinate-system catalog discovery and CRUD. Collation implementation/certification is engine-only; retain the existing evaluation-context and Arrow metadata carriers where qualified.", + "business_cost_of_deferral": "Remote custom collations cannot become local SQL comparators. Remote CRS definitions do not appear as catalog entries in duckdb_coordinate_systems and cannot be resolved/created through a VGI CRS catalog API. Existing metadata preservation must not be advertised as certified transformation or custom collation support.", + "reopening_proof": [ + "Collation: local/remote equality, ordering, NULL/Unicode behavior and supported composition agree at versioned engine/library pins; tests prove optimizer's binary-equality shortcut is sound.", + "CRS: durable authorized list/get and identifier round trip; specify conflicting authority/code versus definitions and both WKT/PROJJSON forms; prove catalog resolver behavior separately from display-only metadata; reject unknown semantic profiles.", + "Do not add empty descriptor placeholders or claim spatial operations from a metadata echo test." + ] + }, + "windows": { + "title": "Standalone window functions", + "disposition": "defer", + "status": "reviewed_deferred_existing_coverage_retained", + "records": [ + "FunctionInfo" + ], + "existing_methods": [ + "aggregate_window_init", + "aggregate_window", + "aggregate_window_batch", + "aggregate_window_destructor" + ], + "new_methods": [], + "scope": [ + "Inventory 2.0 registration and execution separately from existing aggregate-window support; establish whether a portable request/lifecycle can be included now." + ], + "capability_contract": "No new capability selected or advertised for this deferred feature family. Existing aggregate-window support remains covered; defer a new standalone-window function protocol. Port existing callbacks as engine-only work.", + "gates": [], + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/protocol.py:2949-3123 existing aggregate_window_init/window/window_batch/destructor records.", + "engine:src/include/duckdb/function/window_function.hpp:30-39 enumerates PARTITION/PEER/VALID/FRAME bounds.", + "engine:src/include/duckdb/function/window_function.hpp:104-145 and 184-195 defines distinct callbacks and features.", + "python:docs/protocol/vgi-protocol-proposed-changes.md:439-452 explicitly separates WINDOW_FUNCTION_ENTRY from aggregate windows." + ], + "limits": "Deliberate scope deferral, not implemented or qualified feature coverage." + } + ], + "deferred_candidate_records": [ + "WindowFunctionInfo", + "WindowFrame" + ], + "deferred_candidate_methods": [], + "reason": "Current aggregate RPCs already transmit partition data, masks, frame ranges and batched result positions. The 2.0 standalone WindowFunction abstraction adds peer/valid bounds, argument ordering, global/local sink/finalize state and delayed streaming. Reusing the aggregate name would conceal missing semantics; matching the new C++ callback layout would leak implementation details.", + "later_release_cost": "Potential new function kind, advertised features and lifecycle records require coordinated SDK updates. A later bounded adapter that can prove exact equivalence with existing aggregate-window messages may avoid a wire change for that subset; do not promise this for general peer/offset/navigation functions.", + "record_inventory_status": "Only already-existing reusable carriers remain in records. Deferred candidate names are excluded from the included freeze inventory; they are not implemented APIs.", + "review_decision": "Existing aggregate-window support remains covered; defer a new standalone-window function protocol. Port existing callbacks as engine-only work.", + "business_cost_of_deferral": "Existing worker aggregates can still be used as qualified windows. Authors cannot register arbitrary standalone ranking/navigation or custom streaming windows through VGI; DuckDB's own built-ins remain available locally.", + "reopening_proof": [ + "Choose a real function unavailable through aggregate-window callbacks and demonstrate pinned-2.0 execution with peer groups, NULL handling, exclusion and argument ordering.", + "Specify portable row/partition identity, bounds, memory ownership, cancellation/destruction, and streaming delayed output; test partition skew and multiple threads.", + "Keep unsupported options rejected and cross-SDK result/cleanup tests before freezing any added fields." + ] + }, + "statements": { + "title": "Raw SQL CONNECT and statement execution", + "disposition": "include", + "status": "bounded_statement_profile_python_reference_qualified_native_entry_pending", + "records": [ + "CatalogQueryPrepareRequest", + "CatalogQueryPrepareResult", + "ClientQueryCapabilities", + "QueryCapabilities", + "QueryParameters" + ], + "existing_methods": [ + "catalog_query_prepare", + "catalog_query_release", + "bind", + "init", + "catalog_transaction_begin", + "catalog_transaction_commit", + "catalog_transaction_rollback", + "catalog_version" + ], + "new_methods": [], + "scope": [ + "Review alongside read-query preparation, but specify writes/DDL, session state, result kinds, transaction effects, and retry rules separately. The read-query contract does not imply these capabilities." + ], + "capability_contract": "Optional vgi.remote.statement.v1 selects statement mode only by explicit client/provider intersection with transaction support. Every request carries the existing transaction handle and provider-default evaluation context. Same prepare/release and TABLE execution RPCs; static vgi.statement.result.v1 output-schema metadata distinguishes command completion/count rows from RETURNING rows. Native vgi_query remains readonly; raw CONNECT and nontransactional/session effects remain deferred.", + "gates": [], + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/sql.py:62-72 SqlStatement explicitly grants no execution rights.", + "python:vgi/catalog/query.py:122-150 accepts only explicit/automatic query modes and matching query profiles.", + "python:docs/protocol/vgi-catalog-query-pushdown-design.md:202 explicitly excludes remote DDL/DML/COPY/ATTACH/session control/multi-statements/CONNECT.", + "engine:src/include/duckdb/catalog/catalog.hpp:88-104 and 414-420 defines independent IS_REMOTE/EXECUTE_QUERY_NODE/EXECUTE_STATEMENT/CONNECT capabilities." + ], + "limits": "Deliberate scope deferral, not implemented or qualified feature coverage." + }, + { + "status": "bounded_statement_reference_qualified_2026_10_25", + "sources": [ + "python:vgi/catalog/query.py new optional statement profile and fixed result contract", + "python:vgi/query_execution.py existing executor command/EOS validation", + "python:tests/catalog/statement_reference.py finite real DuckDB transaction owner", + "python:tests/catalog/test_statement_protocol.py lifecycle and rollback tests", + "python:tests/catalog/test_statement_socket.py separate-process localhost HTTP example", + "native:docs/duckdb-2.0-plan.md October25 native statement boundary" + ], + "limits": "Python real DuckDB1.5.5 finite provider over signed in-process HTTP plus separate-process localhost HTTP passes; no native mutation enablement or executable statement provider in the other six SDKs." + } + ], + "deferred_candidate_records": [], + "deferred_candidate_methods": [], + "reason": "The user requested an executable proof of existing remote-execution reuse. Real transactional DML/DDL can use the current preparation, parameter, transaction and TABLE-result carriers; a new execution framework is not justified. Native TABLE optimization/completion/cache semantics require a separate adapter.", + "later_release_cost": "The selected profile and reserved output-schema metadata change semantic protocol support without changing record layouts or endpoint schema hash. They remain in the unpublished coordinated successor. Raw sessions would need separate lifetime/routing qualification and possibly later contracts.", + "record_inventory_status": "Only existing records and methods are included; removed speculative StatementRequest/StatementResult/catalog_statement_execute proposals. The Python client catalog_query_execute helper is not a wire method.", + "review_decision": "October25 user retained named-type/ENUM deferral and reopened bounded explicit DML/DDL reuse proof. Extend existing profile/mode and result-schema semantics; no new RPC or request/response field. Keep mutation-aware native statement entry, raw CONNECT, scripts, transaction control and session routing unimplemented.", + "business_cost_of_deferral": "The bounded Python reference can exercise explicit transactional DML/DDL through current RPCs. Native SQL mutation delegation, full CONNECT sessions, scripts and a general production SQL provider remain unavailable.", + "reopening_proof": [ + "Start with explicit one-statement provider-dialect DML/DDL only; forbid scripts, ATTACH/DETACH, transaction-control and session mutations until explicitly modeled.", + "Prove zero/count/row result modes, finite and streaming results, rollback on body/finalize failure, cancellation, ambiguous-dispatch no retry, and no read-result cache reuse for mutations.", + "For raw CONNECT separately prove session isolation/pooling, credentials, disconnect/rollback and engine routing on 2.0; do not infer from EXECUTE_QUERY_NODE." + ] + }, + "multi_input": { + "title": "Multiple relational inputs", + "disposition": "defer", + "status": "reviewed_deferred_existing_coverage_retained", + "records": [ + "InitRequest" + ], + "existing_methods": [], + "new_methods": [], + "scope": [ + "Review the [existing engine proposal](multi_input_table_functions.md) for per-input identity/schema, ordering, backpressure, completion, cancellation, and transaction semantics. Include a wire contract only after the execution model has a working feasibility test." + ], + "capability_contract": "No new capability selected or advertised for this deferred feature family. Defer first-class multiple relational input wire until an engine scheduling spike succeeds; keep the spike on the engine roadmap because business value is concrete. Existing tagged single-input exchange is a bounded workaround, not equivalent support.", + "gates": [], + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "engine@pin:src/planner/binder/tableref/bind_table_function.cpp:174-184 rejects a second subquery.", + "native:docs/multi_input_table_functions.md:57-85 explains transaction/composability costs and the working tagged-input prototype.", + "native:docs/multi_input_table_functions.md:95-107 explicitly labels first-class execution feasibility unproven." + ], + "limits": "Deliberate scope deferral, not implemented or qualified feature coverage." + } + ], + "deferred_candidate_records": [ + "RelationalInputInfo", + "RelationalInputState" + ], + "deferred_candidate_methods": [], + "reason": "Pinned engine still rejects more than one TABLE subquery. The existing proposal proves only the tagged-union workaround, not N-child scheduling. An input_id field cannot by itself specify which input is ready/finished, buffering, cancellation or HTTP continuation ownership.", + "later_release_cost": "New per-input schema/identity and completion/lifecycle transport is likely additive as a separate function profile/method, but entails a full coordinated SDK release. Replacing the existing single-input envelope would be major. Waiting for scheduling evidence avoids freezing a misleading universal input-ID field and later changing it again.", + "record_inventory_status": "Only already-existing reusable carriers remain in records. Deferred candidate names are excluded from the included freeze inventory; they are not implemented APIs.", + "review_decision": "Defer first-class multiple relational input wire until an engine scheduling spike succeeds; keep the spike on the engine roadmap because business value is concrete. Existing tagged single-input exchange is a bounded workaround, not equivalent support.", + "business_cost_of_deferral": "Multi-relation ML, bioinformatics, graph and spatial operators lose separate-input streaming and composability ergonomics. Passing table names to private connections can lose caller transaction visibility; recommend the already-demonstrated same-query tagged-input workaround where full buffering is acceptable instead of presenting private connections as equivalent.", + "reopening_proof": [ + "Two heterogeneous bound TABLE subqueries delivered to one operator in caller transaction, including uncommitted data and one empty input.", + "Small-build/large-stream example demonstrates bounded buffering, input completion and no deadlock; verify cancellation and errors on either input.", + "Prove subprocess and HTTP continuation behavior, transaction propagation, ordering guarantees and input schema drift before freezing portable messages." + ] + }, + "cleanup": { + "title": "Existing protocol cleanup", + "disposition": "include", + "status": "column_default_schema_and_macro_migrations_implemented", + "records": [ + "ColumnPath", + "ConstraintDefinition", + "MacroOverload", + "CatalogAttachResult", + "SqlExpression" + ], + "existing_methods": [], + "new_methods": [], + "scope": [ + "Review inconsistent names/types, scalar default-schema absence, duplicate operations, and weak metadata conventions together. Decide whether a clean major change is preferable to parallel legacy/new forms. Nested default search paths are not an upstream capability at the current pin." + ], + "capability_contract": "Canonical shapes belong to the successor major, not an optional legacy fallback. Preserve semantic SQL profiles and existing operation requirements; no family-only macro read/create checks. Native1.5 rejects unsupported default-schema absence. No new generic cleanup capability.", + "gates": [ + "P1", + "P2", + "P3" + ], + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:docs/protocol/shared-catalog-values.md:168,176,179,896", + "python:docs/protocol/vgi-protocol-proposed-changes.md:43,56,184,714", + "python:vgi/catalog/catalog_interface.py:336,667,679", + "python:vgi/catalog/descriptors.py:951,963", + "python:vgi/argument_spec.py:83", + "native:src/storage/vgi_catalog.cpp:115", + "native:src/vgi_catalog_api.cpp:3003", + "haybarn:src/include/duckdb/catalog/catalog.hpp:438", + "haybarn:src/catalog/catalog.cpp:715,1309" + ], + "limits": "Selected changes are unimplemented and must land with all generated schemas before freeze." + }, + { + "status": "nullable_default_schema_wire_and_15_rejection_implemented", + "checkpoint": "nullable_default_schema_checkpoint_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/catalog/descriptors.py", + "vgi/schema_path.py", + "vgi/worker.py", + "vgi/codegen/ts_types.py", + "tests/catalog/test_nullable_default_schema.py", + "conformance/catalog-contracts.json", + "native:src/vgi_catalog_api.cpp", + "native:test/run_default_schema.py" + ], + "limits": "Native 1.5 rejects absence before attachment publication; this prepares optional-default representation for the later native2 port, not native1.5 execution without a default. Existing omitted provider constructor defaults can remain main." + }, + { + "status": "canonical_macro_family_implementation", + "checkpoint": "macro_family_checkpoint_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "vgi/catalog/macros.py", + "conformance/catalog-contracts.json", + "native:src/vgi_macro_contract.cpp", + "native:src/storage/vgi_macro_set.cpp" + ], + "limits": "Native SQL CREATE and native2 remain separate; no first-overload fallback." + } + ], + "review_decisions": { + "already_covered": [ + "SchemaPath/ColumnPath preserve raw components and replace flat or ambiguous dotted identifiers.", + "Canonical ConstraintDefinition/ConstraintInfo replace parallel legacy constraint arrays; retain this single authoritative shape.", + "Existing flat/nested column operations share changed path fields; no duplicate field mutation RPC family.", + "Typed function defaults are authoritative; vgi_default is explicitly discovery-only, not a competing executable default. Keeping its display metadata is not itself a correctness gap.", + "Canonical DEFAULT/USING payloads and nullable CatalogAttachResult.default_schema are implemented across the coordinated development snapshot without new capability flags." + ], + "recommended_new_wire": "Implemented nullable scalar default_schema: NULL means no implicit default; non-NULL is a nonempty raw root identifier without NUL. Explicit absence never becomes main. Native1.5 rejects absence before publication; native2 optional-return adaptation remains later work.", + "macro_canonicalization_decision": "Implemented one required nonempty overload-family representation; single definitions become one-element families through local authoring helpers. Legacy parent wire fields are removed together with native1.5 family materialization and SDK migration. Function argument defaults remain unchanged; no new capability flag.", + "remaining_P1": "Continue remaining column/optimizer semantics and macro production/general-binding qualification. Audit any further inconsistency by named field, not blanket renaming or reserved metadata bags.", + "evidence": [ + "python:docs/protocol/shared-catalog-values.md:168,176,179,896", + "python:docs/protocol/vgi-protocol-proposed-changes.md:43,56,184,714", + "python:vgi/catalog/catalog_interface.py:336,667,679", + "python:vgi/catalog/descriptors.py:951,963", + "python:vgi/argument_spec.py:83", + "native:src/storage/vgi_catalog.cpp:115", + "native:src/vgi_catalog_api.cpp:3003", + "haybarn:src/include/duckdb/catalog/catalog.hpp:438", + "haybarn:src/catalog/catalog.cpp:715,1309" + ], + "filter_encodings": "Preserve current filter_encodings capability/inventory fields in this package. The old proposal recommends removal because Filter v2 is mandatory, but no concrete consumer conflict or simplification proof is established by this review. Do not broaden cleanup into unproven removals." + }, + "implementation_work": [ + "Complete remaining column/optimizer semantics and release qualification; selected wire cleanup migrations are implemented." + ] + }, + "identity": { + "title": "Full catalog identity", + "disposition": "existing_contract_plus_engine_port", + "status": "nullable_default_wire_implemented_native2_identity_port_pending", + "records": [ + "CatalogObjectRef", + "SchemaPath", + "ColumnPath" + ], + "existing_methods": [ + "catalog_schemas", + "catalog_schema_get", + "catalog_contents" + ], + "new_methods": [], + "scope": [ + "Raw paths, nested ownership, nullable default schema, complete metadata/result cache identity and dependency invalidation. Never split a SQL rendering." + ], + "capability_contract": "Existing raw SchemaPath/ColumnPath/CatalogObjectRef carries attachment-relative identity without engine aliases/OIDs or cross-attachment graphs. Nullable default_schema now preserves absent versus specified, with no new capability. Non-NULL is one raw identifier; nested defaults are not encoded. Native1.5 rejects absence; full-path caches/schema tree and native2 optional default behavior remain adapter work.", + "gates": [ + "P1", + "M1", + "M3" + ], + "evidence": [ + { + "status": "scope_and_field_mapping_reviewed_2026_10_23", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "limits": "Source review only; remaining implementation and qualification tasks are explicit. No new runtime evidence." + }, + { + "status": "nullable_default_schema_wire_and_15_rejection_implemented", + "checkpoint": "nullable_default_schema_checkpoint_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/catalog/descriptors.py", + "vgi/schema_path.py", + "vgi/worker.py", + "vgi/codegen/ts_types.py", + "tests/catalog/test_nullable_default_schema.py", + "conformance/catalog-contracts.json", + "native:src/vgi_catalog_api.cpp", + "native:test/run_default_schema.py" + ], + "limits": "Native 1.5 rejects absence before attachment publication; this prepares optional-default representation for the later native2 port, not native1.5 execution without a default. Existing omitted provider constructor defaults can remain main." + } + ], + "implementation_work": [ + "Native2 optional default schema plus full-path ownership/lookup/recursive scans/cache identities; native1.5 rejects absence and nested paths safely." + ] + } + }, + "audit_fields": [ + { + "id": "audit-057", + "source_line": 57, + "section": "Shared creation fields and object identity", + "field": "`CreateInfo.type`", + "audit_evidence": [ + "Existing", + "Carried as native dispatch and choice of RPC. Unsupported object kinds reject; see the object inventory below." + ], + "area": "dependencies", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:57" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain engine dispatch by CatalogType and existing object-specific RPCs. Unknown/unsupported creation kinds reject before effects; do not send a DuckDB enum as a universal create request.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-058", + "source_line": 58, + "section": "Shared creation fields and object identity", + "field": "`qualified_name` and object-name accessors", + "audit_evidence": [ + "2.0 replaces public catalog/schema/name strings with `QualifiedName` and `Identifier`", + "Gap. Resolve the owning catalog and preserve every raw schema component; send the object name separately. Port accessors rather than assembling or splitting dotted SQL." + ], + "area": "dependencies", + "disposition": "include", + "target": "CatalogObjectRef.schema_path/name", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/schema_path.py", + "vgi/catalog/dependencies.py", + "tests/test_schema_paths_v2.py", + "native:src/vgi_rpc_types.cpp", + "native:src/storage/vgi_schema_set.cpp", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:58" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use existing attachment-relative raw SchemaPath/name or CatalogObjectRef fields. Schema objects use parents plus leaf; table-owned constraints use owner_table. Never split dotted names, send aliases/OIDs, or silently flatten nested ownership. No additional object-identity RPC is required.", + "remaining_implementation_or_qualification": [ + "Native2 schema-tree/typed-identifier/recursive lookup port and nested lifecycle qualification remain open." + ] + }, + { + "id": "audit-059", + "source_line": 59, + "section": "Shared creation fields and object identity", + "field": "`on_conflict`", + "audit_evidence": [ + "Existing", + "ERROR, IGNORE, REPLACE are carried by `MapOnConflict`. Its default maps other values, including ALTER, to ERROR. Make supported values explicit and reject any unsupported value; do not imply every conflict enum is accepted by every grammar production." + ], + "area": "dependencies", + "disposition": "existing_contract", + "target": "Existing object-specific on_conflict: OnConflict", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:59" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep existing OnConflict ERROR/IGNORE/REPLACE. IGNORE preserves the existing object; REPLACE validates and performs the complete provider operation atomically. Schema REPLACE is a non-cascading RESTRICT drop plus create. Do not claim arbitrary dependency-safe replacement for an unqualified provider.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-060", + "source_line": 60, + "section": "Shared creation fields and object identity", + "field": "`temporary`", + "audit_evidence": [ + "Existing", + "Engine-managed routing to the temporary catalog; the binder rejects a temporary object explicitly assigned to an incompatible catalog. No remote temporary-object field is required merely to preserve this routing." + ], + "area": "dependencies", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:60" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Temporary objects remain engine-owned/local unless a provider explicitly supports a session-lifetime operation; do not map temporary to persisted remote creation silently.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-061", + "source_line": 61, + "section": "Shared creation fields and object identity", + "field": "`internal`", + "audit_evidence": [ + "Existing", + "Engine bookkeeping/access control. Preserve when constructing local catalog entries; do not expose it as a worker privilege switch." + ], + "area": "dependencies", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:61" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Internal-object flags remain engine policy, never a provider privilege bypass.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-062", + "source_line": 62, + "section": "Shared creation fields and object identity", + "field": "`extension_name`", + "audit_evidence": [ + "2.0", + "Engine bookkeeping for extension-owned entries. Preserve where applicable to locally registered objects; ordinary remote DDL does not need an extension-name RPC field." + ], + "area": "dependencies", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:62" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Extension ownership remains engine metadata; it is not a worker installation or ownership request.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-063", + "source_line": 63, + "section": "Shared creation fields and object identity", + "field": "`sql`", + "audit_evidence": [ + "Existing", + "Original/rendered SQL is not sent as a general DDL envelope. Structured table creation and SELECT-only view definitions have separate contracts. This field cannot substitute for auditing those fields." + ], + "area": "dependencies", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:63" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "CreateInfo.sql is diagnostic/original SQL. Semantic fields and the existing SELECT-only view/macro definition carriers remain authoritative; do not replay arbitrary CREATE text.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-064", + "source_line": 64, + "section": "Shared creation fields and object identity", + "field": "`dependencies`", + "audit_evidence": [ + "Existing", + "Gap in remote dependency handling. No dependency list is sent with table/view creation. Clearing view caches after some DDL is not dependency enforcement. Define who checks RESTRICT, executes CASCADE, and rewrites or invalidates dependent definitions." + ], + "area": "dependencies", + "disposition": "include", + "target": "ObjectDependency / provider-derived DependencySnapshot", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "dependency_reference_and_native_15_evidence", + "sources": [ + "docs/protocol/shared-catalog-values.md", + "tests/catalog/test_dependencies.py", + "tests/catalog/test_dependencies_protocol.py", + "native:test/sql/duckdb2/schema_dependencies.test" + ], + "limits": "Shared dependency identities/planning and the durable schema/table/simple-view reference are implemented. Native 1.5 qualifies schema lifecycle and transaction cache isolation. General rename/alter dependency authority, production SDK implementations, contract freeze and native 2.0 remain pending." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:64" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Provider resolves dependencies from authoritative definitions and the transaction-visible schemas; catalog_dependencies is discovery, not mutation authorization. Revalidate/authorize the full semantic rename/type/drop closure atomically. Keep requires/owned, blocks_alter and raw subject/dependent column paths; reject unsupported dependent SQL rewrites. Do not send engine dependency lists as trusted facts.", + "remaining_implementation_or_qualification": [ + "Complete production object-kind enforcement and native2 invalidation remain unqualified; metadata reference is not physical row/constraint/index enforcement." + ] + }, + { + "id": "audit-065", + "source_line": 65, + "section": "Shared creation fields and object identity", + "field": "`comment`", + "audit_evidence": [ + "Existing", + "Lost on native schema/table/view creation if supplied through the API. Schema-create wire already accepts it but the adapter sends null. Table/view create requests lack dedicated fields; later table/view comment RPCs exist. Read metadata does preserve object comments." + ], + "area": "dependencies", + "disposition": "include", + "target": "catalog_schema_create.comment / TableCreateRequest.comment / catalog_view_create.comment", + "status": "contract_reviewed_existing_wire_adapter_gaps", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:65" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse existing object-specific create comment/tags fields and provider-derived dependencies. NULL/empty comment and omitted/empty maps retain their documented meanings; validate whole create/replacement before effects. Schema/view creation metadata is implemented; table creation fields are present but native forwarding remains pending. Do not add generic MetadataPatch/metadata-set methods for already-covered operations.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-066", + "source_line": 66, + "section": "Shared creation fields and object identity", + "field": "`tags`", + "audit_evidence": [ + "Existing; 2.0 uses an insertion-preserving map", + "Same create-side omission as comments. Schema-create wire accepts tags but receives null. Table/view reads preserve tags. No corresponding general native tag mutation path is implemented. Do not assume arbitrary tags are schema/table options." + ], + "area": "dependencies", + "disposition": "include", + "target": "catalog_schema_create.tags / TableCreateRequest.tags / catalog_view_create.tags", + "status": "contract_reviewed_existing_wire_adapter_gaps", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:66" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse existing object-specific create comment/tags fields and provider-derived dependencies. NULL/empty comment and omitted/empty maps retain their documented meanings; validate whole create/replacement before effects. Schema/view creation metadata is implemented; table creation fields are present but native forwarding remains pending. Do not add generic MetadataPatch/metadata-set methods for already-covered operations.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-080", + "source_line": 80, + "section": "Paths and catalog enumeration", + "field": "Schema path on schema/table/view/function/macro/index metadata", + "audit_evidence": [ + "Retain vectors throughout decoding and local ownership. Include `catalog_contents` and per-schema discovery, lazy/eager lookup, scan branches, and FK references." + ], + "area": "identity", + "disposition": "existing_contract", + "target": "SchemaPath", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/schema_path.py", + "vgi/catalog/dependencies.py", + "tests/test_schema_paths_v2.py", + "native:src/vgi_rpc_types.cpp", + "native:src/storage/vgi_schema_set.cpp", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:80" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use existing attachment-relative raw SchemaPath/name or CatalogObjectRef fields. Schema objects use parents plus leaf; table-owned constraints use owner_table. Never split dotted names, send aliases/OIDs, or silently flatten nested ownership. No additional object-identity RPC is required.", + "remaining_implementation_or_qualification": [ + "Native2 schema-tree/typed-identifier/recursive lookup port and nested lifecycle qualification remain open." + ] + }, + { + "id": "audit-081", + "source_line": 81, + "section": "Paths and catalog enumeration", + "field": "`SupportsNestedSchemas()` and schema parent links", + "audit_evidence": [ + "Implement a schema tree before opting in. Flat `VgiSchemaSet` entries cannot represent parent/child ownership." + ], + "area": "identity", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:81" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep SupportsNestedSchemas disabled until the native schema tree, parent ownership and recursive traversal are implemented. Existing raw-path wire records need no duplicate identity type.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-082", + "source_line": 82, + "section": "Paths and catalog enumeration", + "field": "`CreateSchemaInfo` schema name convention", + "audit_evidence": [ + "Use `SchemaName()`, `SchemaCatalog()`, `ParentSchemas()`, and `IsNested()`. Its qualified name reserves the object-name position; treating it like a table name selects the wrong component." + ], + "area": "identity", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:82" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Port SchemaName/SchemaCatalog/ParentSchemas/IsNested accessors in the native2 adapter; preserve complete component paths without using table-name layout assumptions.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-083", + "source_line": 83, + "section": "Paths and catalog enumeration", + "field": "`EntryLookupInfo` and `LookupEntry`", + "audit_evidence": [ + "Port typed names and lookup behavior for each object kind, including missing-entry policy and overload lookup where applicable." + ], + "area": "identity", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:83" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Port EntryLookupInfo/LookupEntry and missing-entry/overload rules per object kind; no engine pointers or lookup AST crosses the wire.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-084", + "source_line": 84, + "section": "Paths and catalog enumeration", + "field": "`GetDefaultSchema()`", + "audit_evidence": [ + "Return the new `optional` correctly; preserve absent versus specified defaults. At this pin, nested schemas cannot be used in the search path; keep that upstream rejection distinct from VGI's full-path object lookup support." + ], + "area": "identity", + "disposition": "existing_contract", + "target": "CatalogAttachResult.default_schema: nullable UTF-8 raw top-level identifier", + "status": "nullable_wire_implemented_native15_rejection_native2_mapping_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:src/vgi_catalog_api.cpp:2600", + "tests/test_generated_ts_types.py", + "tests/catalog/test_serialization.py", + "native:docs/duckdb-2.0-catalog-audit.md:84" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "nullable_default_schema_wire_and_15_rejection_implemented", + "checkpoint": "nullable_default_schema_checkpoint_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/catalog/descriptors.py", + "vgi/schema_path.py", + "vgi/worker.py", + "vgi/codegen/ts_types.py", + "tests/catalog/test_nullable_default_schema.py", + "conformance/catalog-contracts.json", + "native:src/vgi_catalog_api.cpp", + "native:test/run_default_schema.py" + ], + "limits": "Native 1.5 rejects absence before attachment publication; this prepares optional-default representation for the later native2 port, not native1.5 execution without a default. Existing omitted provider constructor defaults can remain main." + } + ], + "decision": "Implemented nullable default_schema in the coordinated successor: NULL means no implicit default; non-NULL is a nonempty/NUL-free raw root identifier. No empty-string coercion, main substitution or nested path encoding. Python/declarative clients and SDKs retain explicit absence. Native1.5 rejects absence before publishing attachment or registering settings.", + "remaining_implementation_or_qualification": [ + "Map the nullable wire value to optional and qualify lookup/CREATE/search-path behavior in the native2 port." + ] + }, + { + "id": "audit-085", + "source_line": 85, + "section": "Paths and catalog enumeration", + "field": "Schema `Scan` overloads and recursive scans", + "audit_evidence": [ + "Implement the required context/transaction paths. Current context-free `VgiSchemaEntry::Scan` throws. Recursive/committed schema-tree enumeration must be tested explicitly, not inferred from a successful context-based flat scan." + ], + "area": "identity", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:85" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Implement context/transaction/committed and recursive schema scans in the native2 schema tree; qualification is separate from raw-path protocol support.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-086", + "source_line": 86, + "section": "Paths and catalog enumeration", + "field": "Cache keys and invalidation", + "audit_evidence": [ + "Identify objects by attachment, full schema path, kind, and raw name. Test `a.x.t` versus `b.x.t`, and schema component `\"a.b\"` versus components `a`, `b`. Propagate that distinction through metadata/result caches, retained entries, version reloads, and prepared-statement rebinding." + ], + "area": "identity", + "disposition": "include", + "target": "Local cache identity: attachment/auth/provider/version + raw schema_path/kind/name + transaction/projection/revision", + "status": "contract_reviewed_native2_cache_port_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/schema_path.py", + "vgi/catalog/dependencies.py", + "tests/test_schema_paths_v2.py", + "native:src/vgi_rpc_types.cpp", + "native:src/storage/vgi_schema_set.cpp", + "native:src/include/vgi_result_cache.hpp", + "native:src/storage/vgi_catalog_set.cpp", + "native:src/storage/vgi_transaction.cpp", + "tests/catalog/test_schema_options_protocol.py", + "native:docs/duckdb-2.0-catalog-audit.md:86" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse full attachment/provider/auth/version identity plus raw full schema path, object kind and raw name in metadata/result-cache keys. Include agreement projection and transaction snapshot where applicable. Never collapse a.x.t, b.x.t, and schema component a.b. Invalidation covers descendants and dependency closure; handles/retained entries remain transaction-owned where metadata is mutable.", + "remaining_implementation_or_qualification": [ + "Native cache keys still contain a flat schema_name string. Replace with full-path identity as part of native2 port; no new wire identity record is required." + ] + }, + { + "id": "audit-087", + "source_line": 87, + "section": "Paths and catalog enumeration", + "field": "Schema/drop/rename dependencies", + "audit_evidence": [ + "Invalidate the affected descendants and dependent objects. A leaf-name cache purge is insufficient once distinct parents can contain the same name." + ], + "area": "dependencies", + "disposition": "include", + "target": "ObjectDependency + existing schema/table/view drop/rename operations", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "dependency_reference_and_native_15_evidence", + "sources": [ + "docs/protocol/shared-catalog-values.md", + "tests/catalog/test_dependencies.py", + "tests/catalog/test_dependencies_protocol.py", + "native:test/sql/duckdb2/schema_dependencies.test" + ], + "limits": "Shared dependency identities/planning and the durable schema/table/simple-view reference are implemented. Native 1.5 qualifies schema lifecycle and transaction cache isolation. General rename/alter dependency authority, production SDK implementations, contract freeze and native 2.0 remain pending." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:87" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Provider resolves dependencies from authoritative definitions and the transaction-visible schemas; catalog_dependencies is discovery, not mutation authorization. Revalidate/authorize the full semantic rename/type/drop closure atomically. Keep requires/owned, blocks_alter and raw subject/dependent column paths; reject unsupported dependent SQL rewrites. Do not send engine dependency lists as trusted facts.", + "remaining_implementation_or_qualification": [ + "Complete production object-kind enforcement and native2 invalidation remain unqualified; metadata reference is not physical row/constraint/index enforcement." + ] + }, + { + "id": "audit-100", + "source_line": 100, + "section": "Schema fields", + "field": "`CreateSchemaInfo.qualified_name`", + "audit_evidence": [ + "2.0 representation", + "Gap for nested names; shared identity rules above." + ], + "area": "schemas", + "disposition": "existing_contract", + "target": "SchemaCreateRequest.path", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/schema_path.py", + "vgi/catalog/dependencies.py", + "tests/test_schema_paths_v2.py", + "native:src/vgi_rpc_types.cpp", + "native:src/storage/vgi_schema_set.cpp", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:100" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use existing attachment-relative raw SchemaPath/name or CatalogObjectRef fields. Schema objects use parents plus leaf; table-owned constraints use owner_table. Never split dotted names, send aliases/OIDs, or silently flatten nested ownership. No additional object-identity RPC is required.", + "remaining_implementation_or_qualification": [ + "Native2 schema-tree/typed-identifier/recursive lookup port and nested lifecycle qualification remain open." + ] + }, + { + "id": "audit-101", + "source_line": 101, + "section": "Schema fields", + "field": "`CreateSchemaInfo.options`", + "audit_evidence": [ + "2.0", + "Rejected by inherited `SupportsCreateSchema()` when non-empty. To support WITH, define option names, types, expression evaluation, persistence, and worker capabilities. Schema-create has no general options payload." + ], + "area": "schemas", + "disposition": "include", + "target": "catalog_schema_create.options: TypedValueBatch IPC", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "native:docs/duckdb-2.0-catalog-audit.md:101" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep implemented typed option contracts: schema/defaults plus create/alter allowlists; SET uses TypedValueBatch exact schema/metadata and distinguishes typed NULL; RESET is a list of names and cannot reset required/no-default values. Whole-candidate validation precedes effects. Read/create/alter profiles are separate; transactional mutation requires vgi.transaction.ddl.v1.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-102", + "source_line": 102, + "section": "Schema fields", + "field": "Inherited `on_conflict`", + "audit_evidence": [ + "Existing", + "Carried. Port `ShouldReplaceOnConflict()`/nested replacement behavior and test the parent and descendant effects of REPLACE." + ], + "area": "schemas", + "disposition": "existing_contract", + "target": "catalog_schema_create.on_conflict", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "dependency_reference_and_native_15_evidence", + "sources": [ + "docs/protocol/shared-catalog-values.md", + "tests/catalog/test_dependencies.py", + "tests/catalog/test_dependencies_protocol.py", + "native:test/sql/duckdb2/schema_dependencies.test" + ], + "limits": "Shared dependency identities/planning and the durable schema/table/simple-view reference are implemented. Native 1.5 qualifies schema lifecycle and transaction cache isolation. General rename/alter dependency authority, production SDK implementations, contract freeze and native 2.0 remain pending." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:102" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep existing OnConflict ERROR/IGNORE/REPLACE. IGNORE preserves the existing object; REPLACE validates and performs the complete provider operation atomically. Schema REPLACE is a non-cascading RESTRICT drop plus create. Do not claim arbitrary dependency-safe replacement for an unqualified provider.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-103", + "source_line": 103, + "section": "Schema fields", + "field": "Inherited `comment`, `tags`", + "audit_evidence": [ + "Existing", + "Lost at native create: both optional wire arguments are explicitly null. Adapter work can carry them without adding those fields to the protocol." + ], + "area": "schemas", + "disposition": "existing_contract", + "target": "catalog_schema_create.comment/tags; SchemaInfo.comment/tags", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "creation_uses_existing_fields", + "sources": [ + "docs/protocol/shared-catalog-values.md", + "tests/catalog/test_view_metadata_cross_sdk.py", + "tests/catalog/test_schema_metadata_protocol.py", + "native:test/cpp/test_schema_metadata.cpp" + ], + "limits": "Existing comment/tag field semantics reviewed; native 2.0 port and schema metadata mutation remain pending." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "native:test/cpp/test_schema_metadata.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:103" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain implemented schema create/get/list comment/tags fidelity and CatalogMetadataChanges for explicit comment presence plus tag SET/REMOVE. Creation uses existing fields, mutation uses catalog_schema_alter_metadata under its existing DDL profile. Native schema COMMENT syntax remains upstream-rejected at inspected pins; public RPC support is not native SQL support.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-104", + "source_line": 104, + "section": "Schema fields", + "field": "`AlterSchemaInfo.alter_schema_type`", + "audit_evidence": [ + "2.0", + "No VGI schema-alter implementation or option RPC. Reject SET/RESET until implemented." + ], + "area": "schemas", + "disposition": "include", + "target": "catalog_schema_alter_options / catalog_schema_alter_metadata", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:104" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Map supported SET/RESET options to the existing catalog_schema_alter_options RPC and metadata patches to catalog_schema_alter_metadata. The manifest target catalog_schema_options_set does not exist. Reject other engine variants until explicitly supported.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-105", + "source_line": 105, + "section": "Schema fields", + "field": "`SetSchemaOptionsInfo.options`", + "audit_evidence": [ + "2.0", + "Expression map. New negotiated request/SDK behavior needed for supported options; define whether values are evaluated in DuckDB or interpreted by the worker." + ], + "area": "schemas", + "disposition": "include", + "target": "catalog_schema_alter_options.changes: OptionChanges.set_values", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "native:docs/duckdb-2.0-catalog-audit.md:105" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep implemented typed option contracts: schema/defaults plus create/alter allowlists; SET uses TypedValueBatch exact schema/metadata and distinguishes typed NULL; RESET is a list of names and cannot reset required/no-default values. Whole-candidate validation precedes effects. Read/create/alter profiles are separate; transactional mutation requires vgi.transaction.ddl.v1.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-106", + "source_line": 106, + "section": "Schema fields", + "field": "`ResetSchemaOptionsInfo.options`", + "audit_evidence": [ + "2.0", + "Identifier set. RESET is removal/restoration of defaults, not SET to an empty string or null. Needs a distinct defined operation." + ], + "area": "schemas", + "disposition": "include", + "target": "catalog_schema_alter_options.changes: OptionChanges.reset_names", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "native:docs/duckdb-2.0-catalog-audit.md:106" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep implemented typed option contracts: schema/defaults plus create/alter allowlists; SET uses TypedValueBatch exact schema/metadata and distinguishes typed NULL; RESET is a list of names and cannot reset required/no-default values. Whole-candidate validation precedes effects. Read/create/alter profiles are separate; transactional mutation requires vgi.transaction.ddl.v1.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-107", + "source_line": 107, + "section": "Schema fields", + "field": "Schema `DropInfo` path, `if_not_found`, `cascade`", + "audit_evidence": [ + "Path changes in 2.0", + "Flags are carried today; path is flat. Extend path handling and qualify actual subtree/dependency deletion and rollback." + ], + "area": "schemas", + "disposition": "existing_contract", + "target": "catalog_schema_drop.path/ignore_not_found/cascade", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "dependency_reference_and_native_15_evidence", + "sources": [ + "docs/protocol/shared-catalog-values.md", + "tests/catalog/test_dependencies.py", + "tests/catalog/test_dependencies_protocol.py", + "native:test/sql/duckdb2/schema_dependencies.test" + ], + "limits": "Shared dependency identities/planning and the durable schema/table/simple-view reference are implemented. Native 1.5 qualifies schema lifecycle and transaction cache isolation. General rename/alter dependency authority, production SDK implementations, contract freeze and native 2.0 remain pending." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:107" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep object-specific DROP with raw identity, ignore_not_found and cascade. Missing ignored targets are no-ops with unchanged version/etag. Provider revalidates and authorizes the entire closure inside its transaction; requires blocks RESTRICT and owned auto-drops its dependent. No graph read reserves a later mutation. Native supported kinds are schemas/tables/views/indexes; other kinds require their own qualified adapters.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-125", + "source_line": 125, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`CreateTableInfo.qualified_name`", + "audit_evidence": [ + "2.0 representation", + "Gap for typed/nested identity; ordinary flat name is carried today." + ], + "area": "tables", + "disposition": "existing_contract", + "target": "TableCreateRequest.schema_path/name", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/schema_path.py", + "vgi/catalog/dependencies.py", + "tests/test_schema_paths_v2.py", + "native:src/vgi_rpc_types.cpp", + "native:src/storage/vgi_schema_set.cpp", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:125" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use existing attachment-relative raw SchemaPath/name or CatalogObjectRef fields. Schema objects use parents plus leaf; table-owned constraints use owner_table. Never split dotted names, send aliases/OIDs, or silently flatten nested ownership. No additional object-identity RPC is required.", + "remaining_implementation_or_qualification": [ + "Native2 schema-tree/typed-identifier/recursive lookup port and nested lifecycle qualification remain open." + ] + }, + { + "id": "audit-126", + "source_line": 126, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`columns`", + "audit_evidence": [ + "Existing, typed names in 2.0", + "Partially carried as an Arrow schema; see each column field below." + ], + "area": "tables", + "disposition": "existing_contract", + "target": "TableCreateRequest.columns: complete Arrow schema IPC", + "status": "contract_reviewed_existing_wire_adapter_gaps", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/logical_types.py", + "tests/catalog/test_column_paths_protocol.py", + "native:docs/duckdb-2.0-catalog-audit.md:126" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use the existing exact Arrow schema/field representation for raw names, type, nullability and recursive metadata; reject unsupported logical-type profiles instead of coercing them. CREATE carries all fields; ADD carries exactly one field matching the final target component; TYPE carries only a nullable type field and must preserve unrelated attributes through their own operations.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-127", + "source_line": 127, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`constraints`", + "audit_evidence": [ + "Existing", + "NOT NULL, UNIQUE, PRIMARY KEY, CHECK, FK are encoded separately. Enforcement and complete metadata round trips need worker-specific tests." + ], + "area": "tables", + "disposition": "existing_contract", + "target": "TableCreateRequest.constraint_definitions", + "status": "contract_reviewed_canonical_contract_implemented_bounded", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "vgi/catalog/constraints.py", + "tests/catalog/test_constraints_public_lifecycle.py", + "native:src/vgi_constraint_contract.cpp", + "manifest:regression_checkpoint_2026_10_23", + "native:docs/duckdb-2.0-catalog-audit.md:127" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Canonical constraint_definitions are already required on TableCreateRequest; trusted readback uses ConstraintInfo and revisions. Keep those fields and the canonical apply RPC. Native1.5 implements a bounded subset and CHECK reads only; arbitrary expression/nested/FK semantics remain explicit reject until qualified.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-128", + "source_line": 128, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`query`", + "audit_evidence": [ + "Existing", + "Engine-managed CTAS binding and physical child plan. VGI's CTAS insert operator creates the table and streams query results; the SELECT is not a table-create RPC field. Qualify rollback if insert fails after create." + ], + "area": "tables", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:128" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "CTAS query planning/execution remains native; send the resolved table definition and stream rows through the existing INSERT lifecycle in the same provider transaction. No query AST field is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-129", + "source_line": 129, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`partition_keys`", + "audit_evidence": [ + "Existing", + "Rejected by inherited `SupportsCreateTable()` for non-empty keys. No table-create wire field. Supporting it requires a defined partition-expression contract and worker capability." + ], + "area": "tables", + "disposition": "include", + "target": "TableCreateRequest.layout: TableLayout.partition_by", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_reference_implementation", + "sources": [ + "tests/catalog/test_table_layout_protocol.py", + "vgi/catalog/layout.py" + ], + "limits": "Wire fields and reference behavior implemented; field review, native 2.0 adapter and production qualification remain outstanding." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "native:docs/duckdb-2.0-catalog-audit.md:129" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain TableLayout/SortKey/SqlExpression with explicit owner/context and explicit sort direction/null order. Layout is storage policy, never scan order. CREATE requires ALL_ROWS; ALTER explicitly chooses FUTURE_WRITES or REWRITE, with NULL meaning unchanged and empty list clear. Revalidate retained expressions and complete candidate; REWRITE publishes only after all data conforms, atomically with metadata/version.", + "remaining_implementation_or_qualification": [ + "Production expression/type/context matrices and native SQL adapters remain pending; existing fixture rewrite tests do not qualify arbitrary providers." + ] + }, + { + "id": "audit-130", + "source_line": 130, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`sort_keys`", + "audit_evidence": [ + "Existing", + "Rejected by the same guard. No table-create wire field. Define expression ordering semantics if enabled." + ], + "area": "tables", + "disposition": "include", + "target": "TableCreateRequest.layout: TableLayout.sorted_by / SortKey", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_reference_implementation", + "sources": [ + "tests/catalog/test_table_layout_protocol.py", + "vgi/catalog/layout.py" + ], + "limits": "Wire fields and reference behavior implemented; field review, native 2.0 adapter and production qualification remain outstanding." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "native:docs/duckdb-2.0-catalog-audit.md:130" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain TableLayout/SortKey/SqlExpression with explicit owner/context and explicit sort direction/null order. Layout is storage policy, never scan order. CREATE requires ALL_ROWS; ALTER explicitly chooses FUTURE_WRITES or REWRITE, with NULL meaning unchanged and empty list clear. Revalidate retained expressions and complete candidate; REWRITE publishes only after all data conforms, atomically with metadata/version.", + "remaining_implementation_or_qualification": [ + "Production expression/type/context matrices and native SQL adapters remain pending; existing fixture rewrite tests do not qualify arbitrary providers." + ] + }, + { + "id": "audit-131", + "source_line": 131, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`options`", + "audit_evidence": [ + "Existing", + "Rejected by the same guard; current 1.5 runtime confirms WITH rejection. No table-create options payload. This is not a silently ignored clause today." + ], + "area": "tables", + "disposition": "include", + "target": "TableCreateRequest.options: TypedValueBatch IPC", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_reference_implementation", + "sources": [ + "tests/catalog/test_table_layout_protocol.py", + "vgi/catalog/layout.py" + ], + "limits": "Wire fields and reference behavior implemented; field review, native 2.0 adapter and production qualification remain outstanding." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:131" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain implemented table OptionDefinitions/TypedValueBatch/OptionChanges/OptionState with exact metadata, typed NULL, explicit RESET and complete-candidate validation. Use existing table options read/create/alter profiles and transaction DDL prerequisite; no new RPC is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-132", + "source_line": 132, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`BoundCreateTableInfo.schema`", + "audit_evidence": [ + "Existing, schema API changes", + "Engine/local owner reference. Must refer to the correct nested VGI schema; never serialize the pointer." + ], + "area": "tables", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:132" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "BoundCreateTableInfo.schema is a local resolved catalog entry; send raw identity only.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-133", + "source_line": 133, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`base`", + "audit_evidence": [ + "Existing", + "Native create reads the parsed `CreateTableInfo` here. Audit its fields even when the surrounding bound structure compiles." + ], + "area": "tables", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:133" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use the semantic CreateTableInfo fields through existing carriers; the local bound base pointer is not wire data.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-134", + "source_line": 134, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`column_dependency_manager`", + "audit_evidence": [ + "Existing", + "Engine binding data, not sent. Preserve generated-column dependency semantics through the reconstructed column definitions." + ], + "area": "tables", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "engine_only_with_portable_dependency_requirements", + "sources": [ + "docs/protocol/shared-catalog-values.md#dependencies-for-semantic-alter" + ], + "limits": "Native column_dependency_manager stays local; portable generated-column dependencies need the proposed column edge fields." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:134" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Column dependency manager is engine-local; persist provider-resolved raw dependency edges, not its internal structure.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-135", + "source_line": 135, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "Bound `constraints`", + "audit_evidence": [ + "Existing", + "Native serialization uses `base->constraints`, not this separate collection. Compare normalized/implicit constraints with the worker request; do not assume local binding proves worker enforcement." + ], + "area": "constraints", + "disposition": "include", + "target": "ConstraintDefinition / ConstraintInfo", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-136", + "source_line": 136, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`data`", + "audit_evidence": [ + "Existing", + "DuckDB persistent-table storage data. Engine-managed; not a generic remote catalog payload." + ], + "area": "tables", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:136" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Create data/storage internals stay engine-local; row data uses existing streamed writes.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-137", + "source_line": 137, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "Bound `query`", + "audit_evidence": [ + "Existing", + "Logical CTAS plan. Rebuild/port the physical execution hook rather than serializing the plan into DDL." + ], + "area": "tables", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:137" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Bound CTAS query is engine-local and feeds existing streamed INSERT; do not serialize plans.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-138", + "source_line": 138, + "section": "CreateTableInfo and BoundCreateTableInfo", + "field": "`indexes`", + "audit_evidence": [ + "Existing", + "DuckDB index storage data. Distinct from remote index definitions and not directly transferable." + ], + "area": "tables", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:138" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Bound index objects are engine-local. User index definitions use the separately reviewed typed index API; backing indexes remain provider-owned.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-151", + "source_line": 151, + "section": "Every ColumnDefinition field", + "field": "`name`", + "audit_evidence": [ + "Carried", + "Carried", + "Port `Identifier`; preserve quoted names and distinguish top-level names from nested paths." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "Arrow Field.name / ColumnPath raw components", + "status": "contract_reviewed_existing_wire_adapter_gaps", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/logical_types.py", + "tests/catalog/test_column_paths_protocol.py", + "native:docs/duckdb-2.0-catalog-audit.md:151" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use the existing exact Arrow schema/field representation for raw names, type, nullability and recursive metadata; reject unsupported logical-type profiles instead of coercing them. CREATE carries all fields; ADD carries exactly one field matching the final target component; TYPE carries only a nullable type field and must preserve unrelated attributes through their own operations.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-152", + "source_line": 152, + "section": "Every ColumnDefinition field", + "field": "`type`", + "audit_evidence": [ + "Carried through Arrow", + "Carried through Arrow", + "Qualify supported type parameters and nested types against the 2.0 Arrow bridge. This audit does not certify every DuckDB logical type." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "Arrow Field.type + required logical-type metadata", + "status": "contract_reviewed_existing_wire_adapter_gaps", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/logical_types.py", + "tests/catalog/test_column_paths_protocol.py", + "native:docs/duckdb-2.0-catalog-audit.md:152" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use the existing exact Arrow schema/field representation for raw names, type, nullability and recursive metadata; reject unsupported logical-type profiles instead of coercing them. CREATE carries all fields; ADD carries exactly one field matching the final target component; TYPE carries only a nullable type field and must preserve unrelated attributes through their own operations.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-153", + "source_line": 153, + "section": "Every ColumnDefinition field", + "field": "`compression_type`", + "audit_evidence": [ + "Lost if supplied", + "Lost if supplied", + "Explicitly reject non-default compression unless a worker contract defines it. DuckDB's storage compression enum is not automatically meaningful remotely." + ], + "area": "columns", + "disposition": "defer", + "target": "Unsupported non-default compression; future provider-defined typed option/profile", + "status": "contract_reviewed_deferred", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:153" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Defer remote compression semantics. Continue rejecting non-default DuckDB compression before CREATE/ADD effects; AUTO means provider chooses storage. A DuckDB physical codec enum is not a portable remote storage policy. A future backend can expose a declared typed table option or separately negotiated column profile after demonstrating a real requirement.", + "remaining_implementation_or_qualification": [ + "No current portable compression contract. Later enablement can use existing typed options/Arrow metadata plus a capability; no new RPC is intrinsically required." + ] + }, + { + "id": "audit-154", + "source_line": 154, + "section": "Every ColumnDefinition field", + "field": "`storage_oid`", + "audit_evidence": [ + "Derived locally", + "Derived locally", + "Engine-managed storage identity; do not copy DuckDB OIDs into remote metadata." + ], + "area": "columns", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:154" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Storage OIDs never cross the wire; resolve columns by raw paths.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-155", + "source_line": 155, + "section": "Every ColumnDefinition field", + "field": "`oid`", + "audit_evidence": [ + "Derived locally", + "Derived locally", + "Engine-managed logical index; preserve constraint mapping when reconstructing entries." + ], + "area": "columns", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:155" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Logical OIDs never cross the wire; row/column order and raw names are represented by Arrow and ColumnPath.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-156", + "source_line": 156, + "section": "Every ColumnDefinition field", + "field": "`category`", + "audit_evidence": [ + "Generated versus ordinary category is lost on create", + "No category encoding", + "Carry/validate generated-column semantics or reject them before mutation." + ], + "area": "columns", + "disposition": "include", + "target": "Arrow metadata vgi.column.generation.v1 (virtual/stored)", + "status": "virtual_generated_bounded_validation_passed", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:156" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "virtual_generated_bounded_validation_passed", + "checkpoint": "virtual_generated_columns_checkpoint_2026_10_23", + "limits": "Exact runtime evidence and unsupported paths are tracked in this checkpoint." + } + ], + "decision": "Canonical generated/category metadata reuses SqlExpression IPC and Arrow result types, without a new capability flag. Implement bounded deterministic VIRTUAL CREATE/discovery/read using existing dependency authority and DuckDB execution over physical worker fields. Generic category codec retains stored but current catalog admission rejects it; native ALTER ADD is rejected by the pinned parser.", + "remaining_implementation_or_qualification": [ + "Broader grammar/types/contexts and provider acceptance remain. Generated-table native writes, nested generation, semantic rewrites, unqualified scans and native2 are outside this bounded checkpoint." + ] + }, + { + "id": "audit-157", + "source_line": 157, + "section": "Every ColumnDefinition field", + "field": "`expression` for DEFAULT", + "audit_evidence": [ + "Carried as Arrow field metadata `default`", + "**Lost**", + "Reuse one column encoder for create/add. Recorder probe confirms `ADD ... DEFAULT 9` reaches the worker with empty metadata." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "Arrow metadata vgi.column.default.v1: SqlExpression IPC base64", + "status": "canonical_payload_implemented_bounded_15_qualification", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:157" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "canonical_column_payloads_implemented_on_15", + "sources": [ + "vgi/protocol.py", + "vgi/client/catalog_mixin.py", + "vgi/worker.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/test_column_expression_cli.py", + "native:src/vgi_column_expression.cpp", + "native:test/sql/duckdb2/column_paths.test", + "conformance/catalog-contracts.json" + ], + "checkpoint": "column_expression_checkpoint_2026_10_23", + "limits": "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context." + } + ], + "decision": "Implemented mandatory successor representation: canonical vgi.column.default.v1 metadata and SET DEFAULT carry SqlExpression with explicit context/owner. SQL NULL is a declared default; absent metadata means no default; DROP is the existing explicit operation. No new default capability or scalar READ gate. Providers own per-omitted-row evaluation and explicit ADD population. Native 1.5 materializes only deterministic constant defaults as typed literals while retaining original canonical metadata; unsupported grammar/context rejects before effects.", + "remaining_implementation_or_qualification": [ + "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context.", + "Native 2.0 binding and complete provider/transport release qualification remain open." + ] + }, + { + "id": "audit-158", + "source_line": 158, + "section": "Every ColumnDefinition field", + "field": "`expression` for GENERATED", + "audit_evidence": [ + "**Lost**", + "No generated-expression encoding", + "Current create succeeds and the reconstructed column is ordinary. Existing read path already recognizes `generated_expression`; align outbound metadata and worker behavior with that contract." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "Arrow metadata vgi.column.generated.v1: SqlExpression IPC base64", + "status": "virtual_generated_bounded_validation_passed", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:158" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "virtual_generated_bounded_validation_passed", + "checkpoint": "virtual_generated_columns_checkpoint_2026_10_23", + "limits": "Exact runtime evidence and unsupported paths are tracked in this checkpoint." + } + ], + "decision": "Canonical generated/category metadata reuses SqlExpression IPC and Arrow result types, without a new capability flag. Implement bounded deterministic VIRTUAL CREATE/discovery/read using existing dependency authority and DuckDB execution over physical worker fields. Generic category codec retains stored but current catalog admission rejects it; native ALTER ADD is rejected by the pinned parser.", + "remaining_implementation_or_qualification": [ + "Broader grammar/types/contexts and provider acceptance remain. Generated-table native writes, nested generation, semantic rewrites, unqualified scans and native2 are outside this bounded checkpoint." + ] + }, + { + "id": "audit-159", + "source_line": 159, + "section": "Every ColumnDefinition field", + "field": "`comment`", + "audit_evidence": [ + "Lost if supplied", + "Lost if supplied", + "Read path recognizes Arrow field `comment`; table-column COMMENT RPC also exists. Carry metadata or reject API-level values. The tested inline SQL COMMENT spelling is rejected by the 1.5 parser, so it is not a demonstrated silent SQL loss." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "Arrow Field.metadata[comment] / catalog_table_column_comment_set", + "status": "descriptive_metadata_implemented_bounded_validation_passed", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "tests/catalog/test_column_paths_protocol.py:123", + "tests/catalog/test_column_paths_protocol.py:148", + "native:docs/duckdb-2.0-catalog-audit.md:159" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "descriptive_column_metadata_implemented", + "checkpoint": "column_metadata_checkpoint_2026_10_23", + "sources": [ + "python:vgi/catalog/column_metadata.py", + "python:conformance/catalog-contracts.json", + "native:src/vgi_column_metadata.cpp", + "native:test/run_column_metadata.py" + ], + "limits": "No native1.5 SQL column-tags API; no generated-column execution claim." + } + ], + "decision": "Existing Arrow comment/tags keys are now validated and preserved on CREATE/ADD/discovery, without the proposed metadata-create capability. Comment is literal UTF-8; tags is strict UTF-8 JSON object mapping strings to strings. Absent/empty stay distinct; decoded duplicate keys, malformed JSON/UTF-8, unpaired surrogates and non-string values reject before provider effects. Existing unique Arrow metadata-key rule remains. Valid unrelated metadata is retained. Native1.5 imports/emits comments; its private tags member has no public accessor, so tags remain in authoritative Arrow schema. No engine changes or generic metadata RPC.", + "remaining_implementation_or_qualification": [ + "Nested-path reference preserves JSON tags but does not establish strict cross-SDK tag decoding. Native CREATE/ADD currently rejects inline comments/generated/compression; tag install and metadata profile need implementation and parity tests." + ] + }, + { + "id": "audit-160", + "source_line": 160, + "section": "Every ColumnDefinition field", + "field": "`tags`", + "audit_evidence": [ + "Not encoded", + "Not encoded", + "2.0 exposes tags through accessors, but the underlying field existed in 1.5. Define an Arrow metadata convention and worker persistence before supporting it; current reconstruction does not install column tags." + ], + "area": "columns", + "disposition": "include", + "target": "Arrow Field.metadata[tags]: strict UTF-8 JSON string map", + "status": "descriptive_metadata_implemented_bounded_validation_passed", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "tests/catalog/test_column_paths_protocol.py:123", + "tests/catalog/test_column_paths_protocol.py:148", + "native:docs/duckdb-2.0-catalog-audit.md:160" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "descriptive_column_metadata_implemented", + "checkpoint": "column_metadata_checkpoint_2026_10_23", + "sources": [ + "python:vgi/catalog/column_metadata.py", + "python:conformance/catalog-contracts.json", + "native:src/vgi_column_metadata.cpp", + "native:test/run_column_metadata.py" + ], + "limits": "No native1.5 SQL column-tags API; no generated-column execution claim." + } + ], + "decision": "Existing Arrow comment/tags keys are now validated and preserved on CREATE/ADD/discovery, without the proposed metadata-create capability. Comment is literal UTF-8; tags is strict UTF-8 JSON object mapping strings to strings. Absent/empty stay distinct; decoded duplicate keys, malformed JSON/UTF-8, unpaired surrogates and non-string values reject before provider effects. Existing unique Arrow metadata-key rule remains. Valid unrelated metadata is retained. Native1.5 imports/emits comments; its private tags member has no public accessor, so tags remain in authoritative Arrow schema. No engine changes or generic metadata RPC.", + "remaining_implementation_or_qualification": [ + "Nested-path reference preserves JSON tags but does not establish strict cross-SDK tag decoding. Native CREATE/ADD currently rejects inline comments/generated/compression; tag install and metadata profile need implementation and parity tests." + ] + }, + { + "id": "audit-174", + "source_line": 174, + "section": "Constraint fields", + "field": "`Constraint.type`", + "audit_evidence": [ + "Dispatches the four engine constraint kinds. Replace silent fallback for an unknown kind with an explicit unsupported error if the engine adds another." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ConstraintDefinition.kind", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-175", + "source_line": 175, + "section": "Constraint fields", + "field": "`Constraint.backing_index_oid`", + "audit_evidence": [ + "New 2.0 binding/storage association. Engine-local, not a worker index ID." + ], + "area": "constraints", + "disposition": "engine_only", + "target": null, + "status": "pending_native_qualification", + "evidence": [] + }, + { + "id": "audit-176", + "source_line": 176, + "section": "Constraint fields", + "field": "`NotNullConstraint.index`", + "audit_evidence": [ + "Carried as a logical column index. Test remapping with synthetic row IDs and generated columns." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ConstraintDefinition.column_paths", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-177", + "source_line": 177, + "section": "Constraint fields", + "field": "`UniqueConstraint.index` or `columns`", + "audit_evidence": [ + "Single-column index or resolved column-name group becomes an index vector." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ConstraintDefinition.column_paths", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-178", + "source_line": 178, + "section": "Constraint fields", + "field": "`UniqueConstraint.is_primary_key`", + "audit_evidence": [ + "Selects `primary_key_constraints` versus `unique_constraints`; both are present in the wire request." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ConstraintDefinition.kind=PRIMARY_KEY", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-179", + "source_line": 179, + "section": "Constraint fields", + "field": "`CheckConstraint.expression`", + "audit_evidence": [ + "Carried as SQL expression text, without the outer CHECK wrapper. Define supported expression semantics and worker enforcement." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ConstraintDefinition.check_expression / SqlExpression", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-180", + "source_line": 180, + "section": "Constraint fields", + "field": "`ForeignKeyConstraint.pk_columns`, `fk_columns`", + "audit_evidence": [ + "Carried as column names in serialized FK specifications." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ConstraintDefinition.column_paths / ForeignKeyDefinition.referenced_column_paths", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-181", + "source_line": 181, + "section": "Constraint fields", + "field": "`ForeignKeyInfo.schema`, `table`", + "audit_evidence": [ + "Carried, but schema conversion is currently one component. The wire has `referenced_schema_path`; resolve target-engine FK qualification rules before extending nested references. The target structure still has a single schema `Identifier`." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ForeignKeyDefinition.referenced_table", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-182", + "source_line": 182, + "section": "Constraint fields", + "field": "`ForeignKeyInfo.type`", + "audit_evidence": [ + "Engine direction/self-reference bookkeeping, not explicitly sent. Qualify self-references and reverse dependency handling; user FK definitions alone do not establish both." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ConstraintInfo.referenced_constraint / ObjectDependency", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#dependencies-for-semantic-alter" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-183", + "source_line": 183, + "section": "Constraint fields", + "field": "`ForeignKeyInfo.pk_keys`, `fk_keys`", + "audit_evidence": [ + "Bound physical indices, not sent. Recompute locally where required; remote definitions use column names." + ], + "area": "constraints", + "disposition": "engine_only", + "target": null, + "status": "pending_native_qualification", + "evidence": [] + }, + { + "id": "audit-200", + "source_line": 200, + "section": "ALTER TABLE and shared alteration fields", + "field": "`type`", + "audit_evidence": [ + "Dispatch supports ALTER TABLE, view rename, table/view comments, and table-column comments. Other kinds need an explicit capability decision." + ], + "area": "columns", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:200" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "ALTER operation discriminator selects an existing specific RPC. Unknown variants reject; no blanket alter SQL execution endpoint.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-201", + "source_line": 201, + "section": "ALTER TABLE and shared alteration fields", + "field": "`qualified_name`", + "audit_evidence": [ + "2.0 representation change; preserve the full target path." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "CatalogObjectRef", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/schema_path.py", + "vgi/catalog/dependencies.py", + "tests/test_schema_paths_v2.py", + "native:src/vgi_rpc_types.cpp", + "native:src/storage/vgi_schema_set.cpp", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:201" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use existing attachment-relative raw SchemaPath/name or CatalogObjectRef fields. Schema objects use parents plus leaf; table-owned constraints use owner_table. Never split dotted names, send aliases/OIDs, or silently flatten nested ownership. No additional object-identity RPC is required.", + "remaining_implementation_or_qualification": [ + "Native2 schema-tree/typed-identifier/recursive lookup port and nested lifecycle qualification remain open." + ] + }, + { + "id": "audit-202", + "source_line": 202, + "section": "ALTER TABLE and shared alteration fields", + "field": "`if_not_found`", + "audit_evidence": [ + "DuckDB catalog lookup handles a missing target before VGI's alter hook. Several existing RPC calls nevertheless hardcode `ignore_not_found=false`; qualify a target disappearing between lookup and worker execution. This is not evidence that ordinary ALTER IF EXISTS is always broken." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "Existing per-operation ignore_not_found and if_column_exists/if_column_not_exists", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:202" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep ignore_not_found separate from per-column IF EXISTS/IF NOT EXISTS flags. Validate malformed paths/payloads before applying missing-target no-op semantics; missing ignored targets must not alter metadata/version. Native2 adapter must preserve the correct flag for each operation.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-203", + "source_line": 203, + "section": "ALTER TABLE and shared alteration fields", + "field": "`allow_internal`", + "audit_evidence": [ + "Engine access-control bookkeeping; no remote privilege flag required." + ], + "area": "columns", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:203" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "allow_internal remains engine authorization policy; never convey it as a provider bypass.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-204", + "source_line": 204, + "section": "ALTER TABLE and shared alteration fields", + "field": "`bind_mode`", + "audit_evidence": [ + "Engine binding instruction. Respect skipped binding; do not assume every expression has been bound at the hook." + ], + "area": "columns", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:204" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Bind mode is local binder control. Preserve upstream validation/IF EXISTS behavior without a wire binder-mode flag.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-205", + "source_line": 205, + "section": "ALTER TABLE and shared alteration fields", + "field": "`new_dependencies`", + "audit_evidence": [ + "No remote representation or dependency update. Define maintenance and invalidation when expressions/types change." + ], + "area": "dependencies", + "disposition": "include", + "target": "Provider-rebound ObjectDependency graph for the existing ALTER operation", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:205" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Provider resolves dependencies from authoritative definitions and the transaction-visible schemas; catalog_dependencies is discovery, not mutation authorization. Revalidate/authorize the full semantic rename/type/drop closure atomically. Keep requires/owned, blocks_alter and raw subject/dependent column paths; reject unsupported dependent SQL rewrites. Do not send engine dependency lists as trusted facts.", + "remaining_implementation_or_qualification": [ + "Complete production object-kind enforcement and native2 invalidation remain unqualified; metadata reference is not physical row/constraint/index enforcement." + ] + }, + { + "id": "audit-213", + "source_line": 213, + "section": "ALTER TABLE and shared alteration fields", + "field": "`RENAME_COLUMN`: `old_name`, `new_name`", + "audit_evidence": [ + "Carried to column-rename RPC", + "Port typed names and verify dependency/view invalidation." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "catalog_table_column_rename.column_path/new_column_name", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:213" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain existing raw-path column/table operations; the path includes the complete target and ADD definition name equals its final component. Parent paths must exist, literal dots remain literal, parent NULL validity is preserved, and last-field/constraint/dependency checks run before effects. Nested operations require their exact existing add/drop/rename profile plus DDL transaction support when a token is present. Rename/type with unqualified dependent SQL reject rather than silently rewriting it.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-214", + "source_line": 214, + "section": "ALTER TABLE and shared alteration fields", + "field": "`RENAME_TABLE`: `new_table_name`", + "audit_evidence": [ + "Carried to table-rename RPC", + "Preserve schema ownership and qualified dependent references." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "catalog_table_rename.name/new_name", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:214" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain existing raw-path column/table operations; the path includes the complete target and ADD definition name equals its final component. Parent paths must exist, literal dots remain literal, parent NULL validity is preserved, and last-field/constraint/dependency checks run before effects. Nested operations require their exact existing add/drop/rename profile plus DDL transaction support when a token is present. Rename/type with unqualified dependent SQL reject rather than silently rewriting it.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-215", + "source_line": 215, + "section": "ALTER TABLE and shared alteration fields", + "field": "`ADD_COLUMN`: `new_column`, `if_column_not_exists`", + "audit_evidence": [ + "Names/type and IF NOT EXISTS carried; default and other column attributes omitted", + "Fix the column encoder; see expanded statements below." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "catalog_table_column_add.column_path/column_definition/if_column_not_exists", + "status": "descriptive_metadata_implemented_bounded_validation_passed", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "tests/catalog/test_column_paths_protocol.py:123", + "tests/catalog/test_column_paths_protocol.py:148", + "native:docs/duckdb-2.0-catalog-audit.md:215" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "canonical_column_payloads_implemented_on_15", + "sources": [ + "vgi/protocol.py", + "vgi/client/catalog_mixin.py", + "vgi/worker.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/test_column_expression_cli.py", + "native:src/vgi_column_expression.cpp", + "native:test/sql/duckdb2/column_paths.test", + "conformance/catalog-contracts.json" + ], + "checkpoint": "column_expression_checkpoint_2026_10_23", + "limits": "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context." + }, + { + "status": "descriptive_column_metadata_implemented", + "checkpoint": "column_metadata_checkpoint_2026_10_23", + "sources": [ + "python:vgi/catalog/column_metadata.py", + "python:conformance/catalog-contracts.json", + "native:src/vgi_column_metadata.cpp", + "native:test/run_column_metadata.py" + ], + "limits": "No native1.5 SQL column-tags API; no generated-column execution claim." + } + ], + "decision": "Existing Arrow comment/tags keys are now validated and preserved on CREATE/ADD/discovery, without the proposed metadata-create capability. Comment is literal UTF-8; tags is strict UTF-8 JSON object mapping strings to strings. Absent/empty stay distinct; decoded duplicate keys, malformed JSON/UTF-8, unpaired surrogates and non-string values reject before provider effects. Existing unique Arrow metadata-key rule remains. Valid unrelated metadata is retained. Native1.5 imports/emits comments; its private tags member has no public accessor, so tags remain in authoritative Arrow schema. No engine changes or generic metadata RPC.", + "remaining_implementation_or_qualification": [ + "Canonical ADD defaults now use the same codec as CREATE/SET/READ. Strict cross-SDK tags, inline comment/generated support, complete dependencies and native2 binding remain separate work." + ] + }, + { + "id": "audit-216", + "source_line": 216, + "section": "ALTER TABLE and shared alteration fields", + "field": "`REMOVE_COLUMN`: `removed_column`, `if_column_exists`, `cascade`", + "audit_evidence": [ + "Carried", + "Qualify generated-column dependencies, actual cascade behavior, rollback, and column ordering." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "catalog_table_column_drop.column_path/if_column_exists/cascade", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:216" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain existing raw-path column/table operations; the path includes the complete target and ADD definition name equals its final component. Parent paths must exist, literal dots remain literal, parent NULL validity is preserved, and last-field/constraint/dependency checks run before effects. Nested operations require their exact existing add/drop/rename profile plus DDL transaction support when a token is present. Rename/type with unqualified dependent SQL reject rather than silently rewriting it.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-217", + "source_line": 217, + "section": "ALTER TABLE and shared alteration fields", + "field": "`ALTER_COLUMN_TYPE`: `column_path`, `target_type`, `expression`", + "audit_evidence": [ + "Flat column name, target Arrow type, and optional USING SQL are carried in 1.5", + "**2.0 replaces `column_name` with a vector path.** Existing RPC lacks a nested path. Support a single component explicitly; reject deeper paths until the contract is extended." + ], + "area": "columns", + "disposition": "include", + "target": "catalog_table_column_type_change.column_path/column_definition/expression: nullable SqlExpression IPC", + "status": "canonical_payload_implemented_bounded_15_qualification", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:217" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "canonical_column_payloads_implemented_on_15", + "sources": [ + "vgi/protocol.py", + "vgi/client/catalog_mixin.py", + "vgi/worker.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/test_column_expression_cli.py", + "native:src/vgi_column_expression.cpp", + "native:test/sql/duckdb2/column_paths.test", + "conformance/catalog-contracts.json" + ], + "checkpoint": "column_expression_checkpoint_2026_10_23", + "limits": "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context." + } + ], + "decision": "Implemented nullable SqlExpression IPC for USING. NULL requests the provider-qualified ordinary target cast; a present record evaluates against old rows and then casts to the target field. SQL NULL remains distinct. No new USING capability or scalar READ gate. Existing nested/transaction/constraint requirements remain. The durable reference publishes schema/data/version atomically and tests failed-cast rollback.", + "remaining_implementation_or_qualification": [ + "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context.", + "Native 2.0 binding and complete provider/transport release qualification remain open." + ] + }, + { + "id": "audit-218", + "source_line": 218, + "section": "ALTER TABLE and shared alteration fields", + "field": "`SET_DEFAULT`: `column_path`, `expression`", + "audit_evidence": [ + "Set expression or null-expression DROP maps to separate RPCs", + "Same new path gap. Preserve absence versus SQL NULL expression." + ], + "area": "columns", + "disposition": "include", + "target": "catalog_table_column_default_set.expression: required SqlExpression IPC binary", + "status": "canonical_payload_implemented_bounded_15_qualification", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:218" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "canonical_column_payloads_implemented_on_15", + "sources": [ + "vgi/protocol.py", + "vgi/client/catalog_mixin.py", + "vgi/worker.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/test_column_expression_cli.py", + "native:src/vgi_column_expression.cpp", + "native:test/sql/duckdb2/column_paths.test", + "conformance/catalog-contracts.json" + ], + "checkpoint": "column_expression_checkpoint_2026_10_23", + "limits": "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context." + } + ], + "decision": "Implemented mandatory successor representation: canonical vgi.column.default.v1 metadata and SET DEFAULT carry SqlExpression with explicit context/owner. SQL NULL is a declared default; absent metadata means no default; DROP is the existing explicit operation. No new default capability or scalar READ gate. Providers own per-omitted-row evaluation and explicit ADD population. Native 1.5 materializes only deterministic constant defaults as typed literals while retaining original canonical metadata; unsupported grammar/context rejects before effects.", + "remaining_implementation_or_qualification": [ + "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context.", + "Native 2.0 binding and complete provider/transport release qualification remain open." + ] + }, + { + "id": "audit-219", + "source_line": 219, + "section": "ALTER TABLE and shared alteration fields", + "field": "`FOREIGN_KEY_CONSTRAINT`: `fk_table`, `pk_columns`, `fk_columns`, `pk_keys`, `fk_keys`, `type`", + "audit_evidence": [ + "Rejected", + "No implemented ALTER FK path. Keep unsupported unless worker dependency/enforcement operations are defined; physical key vectors are local." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "ForeignKeyDefinition / ConstraintInfo / ObjectDependency; bound key indexes remain local", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-220", + "source_line": 220, + "section": "ALTER TABLE and shared alteration fields", + "field": "`SET_NOT_NULL`: `column_path`", + "audit_evidence": [ + "Flat column RPC carried", + "New path gap. Enforce against existing data and writes." + ], + "area": "columns", + "disposition": "include", + "target": "catalog_table_constraints_apply: canonical ADD NOT_NULL / nested-nullability adapter", + "status": "contract_reviewed_canonical_contract_implemented_bounded", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/constraints.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_constraint_contract.cpp", + "tests/catalog/test_constraints_public_lifecycle.py", + "native:docs/duckdb-2.0-catalog-audit.md:220" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain canonical NOT NULL changes through ConstraintInfo/ConstraintChange with stable identity and expected definition revision; legacy named operations remain adapters, not a second constraint authority. Nested paths additionally need nested-nullability support. Native1.5 reads the current definition and uses constraints_apply; ENFORCE is required for actual native constrained mutation, independently from READ.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-221", + "source_line": 221, + "section": "ALTER TABLE and shared alteration fields", + "field": "`DROP_NOT_NULL`: `column_path`", + "audit_evidence": [ + "Flat column RPC carried", + "New path gap; retain other constraints when removing it." + ], + "area": "columns", + "disposition": "include", + "target": "catalog_table_constraints_apply: stable-key DROP NOT_NULL / nested-nullability adapter", + "status": "contract_reviewed_canonical_contract_implemented_bounded", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/constraints.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_constraint_contract.cpp", + "tests/catalog/test_constraints_public_lifecycle.py", + "native:docs/duckdb-2.0-catalog-audit.md:221" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain canonical NOT NULL changes through ConstraintInfo/ConstraintChange with stable identity and expected definition revision; legacy named operations remain adapters, not a second constraint authority. Nested paths additionally need nested-nullability support. Native1.5 reads the current definition and uses constraints_apply; ENFORCE is required for actual native constrained mutation, independently from READ.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-222", + "source_line": 222, + "section": "ALTER TABLE and shared alteration fields", + "field": "`SET_COLUMN_COMMENT`", + "audit_evidence": [ + "No case in the table-variant switch", + "Actual COMMENT handling uses outer `AlterType::SET_COLUMN_COMMENT`; audit that payload separately below. Do not count this enum spelling as an additional working operation." + ], + "area": "columns", + "disposition": "existing_contract", + "target": "catalog_table_column_comment_set / catalog_view_column_comment_set", + "status": "contract_reviewed_existing_contract_implemented_bounded", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:222" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse dedicated table/view comment and column-comment RPCs. NULL removes the comment, empty string remains a present comment. Table columns use raw ColumnPath (nested comment profile where applicable); view outputs take exactly one raw identifier. Dispatch by actual table/view kind, validate output existence when the owning binder can do so, and invalidate the correct cache. No generic MetadataPatch/object_metadata_set is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-223", + "source_line": 223, + "section": "ALTER TABLE and shared alteration fields", + "field": "`ADD_CONSTRAINT`: `constraint`", + "audit_evidence": [ + "Rejected by VGI; index-related binding can reject earlier", + "Needed for supported ALTER constraint changes and some new ADD COLUMN rewrites. Do not conflate CREATE-time constraint transport with ALTER support." + ], + "area": "constraints", + "disposition": "include", + "target": "catalog_table_constraints_apply / ConstraintDefinition", + "status": "specified_reference_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + } + ] + }, + { + "id": "audit-224", + "source_line": 224, + "section": "ALTER TABLE and shared alteration fields", + "field": "`SET_PARTITIONED_BY`: `partition_keys`", + "audit_evidence": [ + "Rejected", + "Define expression contract and repartitioning/atomicity if enabled." + ], + "area": "tables", + "disposition": "include", + "target": "catalog_table_alter_layout.changes: TableLayoutChange.partition_by", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_reference_implementation", + "sources": [ + "tests/catalog/test_table_layout_protocol.py", + "vgi/catalog/layout.py" + ], + "limits": "Wire fields and reference behavior implemented; field review, native 2.0 adapter and production qualification remain outstanding." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "native:docs/duckdb-2.0-catalog-audit.md:224" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain TableLayout/SortKey/SqlExpression with explicit owner/context and explicit sort direction/null order. Layout is storage policy, never scan order. CREATE requires ALL_ROWS; ALTER explicitly chooses FUTURE_WRITES or REWRITE, with NULL meaning unchanged and empty list clear. Revalidate retained expressions and complete candidate; REWRITE publishes only after all data conforms, atomically with metadata/version.", + "remaining_implementation_or_qualification": [ + "Production expression/type/context matrices and native SQL adapters remain pending; existing fixture rewrite tests do not qualify arbitrary providers." + ] + }, + { + "id": "audit-225", + "source_line": 225, + "section": "ALTER TABLE and shared alteration fields", + "field": "`SET_SORTED_BY`: `orders`", + "audit_evidence": [ + "Rejected", + "Preserve expression, direction, and null order in any new contract." + ], + "area": "tables", + "disposition": "include", + "target": "catalog_table_alter_layout.changes: TableLayoutChange.sorted_by", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_reference_implementation", + "sources": [ + "tests/catalog/test_table_layout_protocol.py", + "vgi/catalog/layout.py" + ], + "limits": "Wire fields and reference behavior implemented; field review, native 2.0 adapter and production qualification remain outstanding." + }, + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "native:docs/duckdb-2.0-catalog-audit.md:225" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain TableLayout/SortKey/SqlExpression with explicit owner/context and explicit sort direction/null order. Layout is storage policy, never scan order. CREATE requires ALL_ROWS; ALTER explicitly chooses FUTURE_WRITES or REWRITE, with NULL meaning unchanged and empty list clear. Revalidate retained expressions and complete candidate; REWRITE publishes only after all data conforms, atomically with metadata/version.", + "remaining_implementation_or_qualification": [ + "Production expression/type/context matrices and native SQL adapters remain pending; existing fixture rewrite tests do not qualify arbitrary providers." + ] + }, + { + "id": "audit-226", + "source_line": 226, + "section": "ALTER TABLE and shared alteration fields", + "field": "`ADD_FIELD`: `column_path`, `new_field`, `if_field_not_exists`", + "audit_evidence": [ + "Rejected", + "Needs nested field path and complete new-column metadata plus worker data rewrite semantics." + ], + "area": "columns", + "disposition": "include", + "target": "catalog_table_column_add.column_path/column_definition/if_column_not_exists", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:226" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain existing raw-path column/table operations; the path includes the complete target and ADD definition name equals its final component. Parent paths must exist, literal dots remain literal, parent NULL validity is preserved, and last-field/constraint/dependency checks run before effects. Nested operations require their exact existing add/drop/rename profile plus DDL transaction support when a token is present. Rename/type with unqualified dependent SQL reject rather than silently rewriting it.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-227", + "source_line": 227, + "section": "ALTER TABLE and shared alteration fields", + "field": "`REMOVE_FIELD`: `column_path`, `if_column_exists`, `cascade`", + "audit_evidence": [ + "Rejected", + "Needs nested field deletion and dependencies." + ], + "area": "columns", + "disposition": "include", + "target": "catalog_table_column_drop.column_path/if_column_exists/cascade", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:227" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain existing raw-path column/table operations; the path includes the complete target and ADD definition name equals its final component. Parent paths must exist, literal dots remain literal, parent NULL validity is preserved, and last-field/constraint/dependency checks run before effects. Nested operations require their exact existing add/drop/rename profile plus DDL transaction support when a token is present. Rename/type with unqualified dependent SQL reject rather than silently rewriting it.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-228", + "source_line": 228, + "section": "ALTER TABLE and shared alteration fields", + "field": "`RENAME_FIELD`: `column_path`, `new_name`", + "audit_evidence": [ + "Rejected", + "Needs path-preserving rename. A field named `\"a.b\"` must remain distinct from path `[\"a\", \"b\"]`." + ], + "area": "columns", + "disposition": "include", + "target": "catalog_table_column_rename.column_path/new_column_name", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "partial_column_path_qualification", + "sources": [ + "tests/catalog/test_column_paths_protocol.py", + "vgi/protocol.py", + "../vgi/test/sql/duckdb2/column_paths.test" + ], + "limits": "Raw paths and the durable reference are implemented; flat 1.5 SQL, defaults, metadata readback, and rollback pass. Shared expressions, complete attributes/dependencies, and native 2.0 binding remain open." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:228" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain existing raw-path column/table operations; the path includes the complete target and ADD definition name equals its final component. Parent paths must exist, literal dots remain literal, parent NULL validity is preserved, and last-field/constraint/dependency checks run before effects. Nested operations require their exact existing add/drop/rename profile plus DDL transaction support when a token is present. Rename/type with unqualified dependent SQL reject rather than silently rewriting it.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-229", + "source_line": 229, + "section": "ALTER TABLE and shared alteration fields", + "field": "`SET_TABLE_OPTIONS`: `table_options` expression map", + "audit_evidence": [ + "Rejected", + "Define supported values/evaluation and worker persistence." + ], + "area": "tables", + "disposition": "include", + "target": "catalog_table_alter_options.changes: OptionChanges.set_values", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:229" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain implemented table OptionDefinitions/TypedValueBatch/OptionChanges/OptionState with exact metadata, typed NULL, explicit RESET and complete-candidate validation. Use existing table options read/create/alter profiles and transaction DDL prerequisite; no new RPC is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-230", + "source_line": 230, + "section": "ALTER TABLE and shared alteration fields", + "field": "`RESET_TABLE_OPTIONS`: `table_options` identifier set", + "audit_evidence": [ + "Rejected", + "Define removal/default restoration separately from SET." + ], + "area": "tables", + "disposition": "include", + "target": "catalog_table_alter_options.changes: OptionChanges.reset_names", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:230" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain implemented table OptionDefinitions/TypedValueBatch/OptionChanges/OptionState with exact metadata, typed NULL, explicit RESET and complete-candidate validation. Use existing table options read/create/alter profiles and transaction DDL prerequisite; no new RPC is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-241", + "source_line": 241, + "section": "ADD COLUMN can execute several statements", + "field": "Simple constant default, possibly a normal cast", + "audit_evidence": [ + "Plain ADD COLUMN", + "Preserve the default in the column payload; current adapter loses it." + ], + "area": "transactions", + "disposition": "existing_contract", + "target": "Existing CREATE/ADD Arrow default metadata + explicit existing-row population", + "status": "canonical_payload_implemented_bounded_15_qualification", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:241" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "canonical_column_payloads_implemented_on_15", + "sources": [ + "vgi/protocol.py", + "vgi/client/catalog_mixin.py", + "vgi/worker.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/test_column_expression_cli.py", + "native:src/vgi_column_expression.cpp", + "native:test/sql/duckdb2/column_paths.test", + "conformance/catalog-contracts.json" + ], + "checkpoint": "column_expression_checkpoint_2026_10_23", + "limits": "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context." + } + ], + "decision": "Implemented mandatory successor representation: canonical vgi.column.default.v1 metadata and SET DEFAULT carry SqlExpression with explicit context/owner. SQL NULL is a declared default; absent metadata means no default; DROP is the existing explicit operation. No new default capability or scalar READ gate. Providers own per-omitted-row evaluation and explicit ADD population. Native 1.5 materializes only deterministic constant defaults as typed literals while retaining original canonical metadata; unsupported grammar/context rejects before effects.", + "remaining_implementation_or_qualification": [ + "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context.", + "Native 2.0 binding and complete provider/transport release qualification remain open." + ] + }, + { + "id": "audit-242", + "source_line": 242, + "section": "ADD COLUMN can execute several statements", + "field": "Non-simple default without IF NOT EXISTS", + "audit_evidence": [ + "ADD with null default, UPDATE existing rows, SET DEFAULT", + "All operations must use coherent transaction state. Prove rollback if UPDATE or the final ALTER fails, including for nontransactional workers that must reject the shape." + ], + "area": "transactions", + "disposition": "include", + "target": "Existing transaction_opaque_data + native engine expanded-ALTER sequence", + "status": "contract_reviewed_existing_lifecycle_native2_qualification_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "native:src/storage/vgi_transaction.cpp", + "native:src/storage/vgi_physical_write.cpp", + "native:src/include/vgi_unary_retry.hpp", + "native:test/run_transactional_write_probe.py", + "tests/catalog/transactional_write_reference.py", + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes", + "manifest:transaction_write_checkpoint_2026_10_22", + "manifest:regression_checkpoint_2026_10_23", + "native:docs/duckdb-2.0-catalog-audit.md:242" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse existing attachment-bound provider transaction, BEGIN/COMMIT/ROLLBACK and streamed init/input/finalize; failed writes invalidate/roll back the whole transaction. No mixed/statement/savepoint/replay profile or coordinator is selected. CTAS and INSERT already have stored-data native1.5 rollback evidence. Expanded native2 ALTER and multiple-DML CTEs remain disabled until complete sequences, RETURNING and failure rollback are qualified. Preserve idempotent completion retries; BEGIN/non-idempotent mutation dispatch is single attempt.", + "remaining_implementation_or_qualification": [ + "No native2 expanded-ALTER/multiple-DML/OLD-NEW/deferred-RETURNING qualification; provider lost-BEGIN orphan cleanup remains provider-owned. Trigger implementation is deferred." + ] + }, + { + "id": "audit-243", + "source_line": 243, + "section": "ADD COLUMN can execute several statements", + "field": "NOT NULL or UNIQUE on the new column", + "audit_evidence": [ + "ADD, then SET NOT NULL and/or ADD CONSTRAINT", + "Check support for the complete operation before accepting it. Current VGI lacks ADD CONSTRAINT, so a partial implementation risks an ADD followed by failure. Establish where validation occurs and test that no remote mutation survives failure." + ], + "area": "transactions", + "disposition": "include", + "target": "Existing provider transaction + canonical constraints_apply for the complete ADD sequence", + "status": "contract_reviewed_existing_lifecycle_native2_qualification_pending", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "native:src/storage/vgi_transaction.cpp", + "native:src/storage/vgi_physical_write.cpp", + "native:src/include/vgi_unary_retry.hpp", + "native:test/run_transactional_write_probe.py", + "tests/catalog/transactional_write_reference.py", + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes", + "manifest:transaction_write_checkpoint_2026_10_22", + "manifest:regression_checkpoint_2026_10_23", + "native:docs/duckdb-2.0-catalog-audit.md:243" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse existing attachment-bound provider transaction, BEGIN/COMMIT/ROLLBACK and streamed init/input/finalize; failed writes invalidate/roll back the whole transaction. No mixed/statement/savepoint/replay profile or coordinator is selected. CTAS and INSERT already have stored-data native1.5 rollback evidence. Expanded native2 ALTER and multiple-DML CTEs remain disabled until complete sequences, RETURNING and failure rollback are qualified. Preserve idempotent completion retries; BEGIN/non-idempotent mutation dispatch is single attempt.", + "remaining_implementation_or_qualification": [ + "No native2 expanded-ALTER/multiple-DML/OLD-NEW/deferred-RETURNING qualification; provider lost-BEGIN orphan cleanup remains provider-owned. Trigger implementation is deferred." + ] + }, + { + "id": "audit-244", + "source_line": 244, + "section": "ADD COLUMN can execute several statements", + "field": "IF NOT EXISTS with NOT NULL or UNIQUE", + "audit_evidence": [ + "Rejected by the target transformer", + "Preserve upstream rejection; no additional worker feature required for this rejected shape." + ], + "area": "transactions", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:244" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Preserve upstream rejection of IF NOT EXISTS combined with NOT NULL/UNIQUE. No worker feature or new transaction profile is needed for rejected syntax.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-245", + "source_line": 245, + "section": "ADD COLUMN can execute several statements", + "field": "IF NOT EXISTS with a non-simple default", + "audit_evidence": [ + "Kept as a plain ALTER", + "Do not assume all nonconstant defaults are materialized by the engine; the worker path still needs defined default behavior." + ], + "area": "transactions", + "disposition": "existing_contract", + "target": "Existing ADD/SET default operation with canonical SqlExpression and explicit population semantics", + "status": "canonical_payload_implemented_bounded_15_qualification", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/sql.py", + "vgi/catalog/evaluation_context.py", + "tests/catalog/expression_reference.py", + "tests/catalog/test_expression_contract.py", + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:245" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + }, + { + "status": "canonical_column_payloads_implemented_on_15", + "sources": [ + "vgi/protocol.py", + "vgi/client/catalog_mixin.py", + "vgi/worker.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/test_column_expression_cli.py", + "native:src/vgi_column_expression.cpp", + "native:test/sql/duckdb2/column_paths.test", + "conformance/catalog-contracts.json" + ], + "checkpoint": "column_expression_checkpoint_2026_10_23", + "limits": "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context." + } + ], + "decision": "Implemented mandatory successor representation: canonical vgi.column.default.v1 metadata and SET DEFAULT carry SqlExpression with explicit context/owner. SQL NULL is a declared default; absent metadata means no default; DROP is the existing explicit operation. No new default capability or scalar READ gate. Providers own per-omitted-row evaluation and explicit ADD population. Native 1.5 materializes only deterministic constant defaults as typed literals while retaining original canonical metadata; unsupported grammar/context rejects before effects.", + "remaining_implementation_or_qualification": [ + "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context.", + "Native 2.0 binding and complete provider/transport release qualification remain open." + ] + }, + { + "id": "audit-258", + "source_line": 258, + "section": "Views and comments", + "field": "`qualified_name` / `GetViewName()`", + "audit_evidence": [ + "2.0 representation", + "Port typed/nested ownership. Flat schema and view name are carried today." + ], + "area": "views", + "disposition": "existing_contract", + "target": "ViewCreateRequest.schema_path/name", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/schema_path.py", + "vgi/catalog/dependencies.py", + "tests/test_schema_paths_v2.py", + "native:src/vgi_rpc_types.cpp", + "native:src/storage/vgi_schema_set.cpp", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:258" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use existing attachment-relative raw SchemaPath/name or CatalogObjectRef fields. Schema objects use parents plus leaf; table-owned constraints use owner_table. Never split dotted names, send aliases/OIDs, or silently flatten nested ownership. No additional object-identity RPC is required.", + "remaining_implementation_or_qualification": [ + "Native2 schema-tree/typed-identifier/recursive lookup port and nested lifecycle qualification remain open." + ] + }, + { + "id": "audit-259", + "source_line": 259, + "section": "Views and comments", + "field": "`query`", + "audit_evidence": [ + "Existing", + "Carried as `query->ToString()`, a SELECT definition. Read path parses that SELECT. Test owner-relative names, nested schemas, and search-path independence." + ], + "area": "views", + "disposition": "existing_contract", + "target": "catalog_view_create.definition / ViewInfo.definition: exactly one SELECT in owner-relative scope", + "status": "contract_reviewed_existing_wire_adapter_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "native:duckdb/src/catalog/catalog_entry/view_catalog_entry.cpp:146", + "native:duckdb/src/planner/binder/tableref/bind_basetableref.cpp:105", + "native:docs/duckdb-2.0-catalog-audit.md:259" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain exactly one SELECT-only definition string with existing owner schema/name and aliases, rather than introducing a second SqlStatement view carrier. Choose engine-derived late binding in the owning catalog/schema; do not promise fixed output types, persistent session search paths or cross-attachment aliases. Providers with narrower SQL/dependency support reject before effects. Native reads must reject extra statements rather than selecting only the first.", + "remaining_implementation_or_qualification": [ + "Current native view parser accepts statements[0] without checking size==1; tighten before release. Owner/default-schema resolution, changed source types and prepared rebind need focused qualification, especially native2." + ] + }, + { + "id": "audit-260", + "source_line": 260, + "section": "Views and comments", + "field": "`aliases`", + "audit_evidence": [ + "Existing", + "**Lost.** Explicit `CREATE VIEW v(alias_name) AS SELECT ... AS original_name` comes back with `original_name` in the 1.5 probe. Preserve explicit output aliases in a defined representation." + ], + "area": "views", + "disposition": "include", + "target": "catalog_view_create.aliases / ViewInfo.aliases", + "status": "contract_reviewed_existing_contract_implemented_bounded", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "manifest:view_aliases_validation", + "native:docs/duckdb-2.0-catalog-audit.md:260" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "implementation_evidence": "Ordered raw aliases implemented across SDKs and native1.5; see view_aliases_validation. Full field/contract review remains pending.", + "decision": "Keep required ordered raw ViewInfo.aliases and nullable create aliases (NULL/[] mean no explicit aliases). Partial aliases rename leading outputs; never split/quote/parse them as SQL. Native initial metadata binding now installs aliases before COMMENT/first SELECT. No duplicate output-name field.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-261", + "source_line": 261, + "section": "Views and comments", + "field": "`types`", + "audit_evidence": [ + "Existing", + "Not persisted by view-create RPC; recomputed when binding the definition. Qualify changed source types and prepared statements. Persist/validate if worker semantics require a fixed output contract." + ], + "area": "views", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:261" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Choose engine-derived view output types at bind/rebind; no persisted fixed-output schema is promised by this release. Incompatible rebinding fails rather than casting silently. A future fixed-output provider contract would be a separately negotiated feature.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-262", + "source_line": 262, + "section": "Views and comments", + "field": "`names`", + "audit_evidence": [ + "Existing", + "Not sent separately. Correct recomputation depends on preserving aliases and the query." + ], + "area": "views", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:262" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Derive output names from exactly one SELECT plus ordered explicit aliases. No duplicate names field is required; preserve aliases at initial metadata binding as already implemented.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-263", + "source_line": 263, + "section": "Views and comments", + "field": "`column_comments_map`", + "audit_evidence": [ + "Existing", + "Not sent on create; read metadata can install column comments. Define writable view-column metadata." + ], + "area": "views", + "disposition": "include", + "target": "catalog_view_create.column_comments / ViewInfo.column_comments / catalog_view_column_comment_set", + "status": "contract_reviewed_existing_contract_implemented_bounded", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "native:docs/duckdb-2.0-catalog-audit.md:263" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain creation column_comments map and ViewInfo.column_comments plus the dedicated view-column comment mutation. Keys are raw exposed names after aliases, values may be empty; NULL mutation removes an entry. Metadata-only providers preserve values but cannot claim SQL output validation; owning binders validate actual targets.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-264", + "source_line": 264, + "section": "Views and comments", + "field": "`binding_mode`", + "audit_evidence": [ + "2.0", + "Engine-managed BIND_ON_CREATE/SKIP_BINDING. The binder skips validation of an existing IF NOT EXISTS view. Do not introduce unconditional rebinding that changes this behavior." + ], + "area": "views", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:264" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep view BIND_ON_CREATE/SKIP_BINDING engine-managed, including existing IF NOT EXISTS behavior. Provider authorization/structural validation still runs; do not force SQL binding of ignored definitions.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-265", + "source_line": 265, + "section": "Views and comments", + "field": "`security_type`", + "audit_evidence": [ + "**2.0**", + "**Gap with security consequences.** The new SECURE_VIEW flag is an optimization barrier. Existing create/read representations have no flag. Preserve it across worker storage, discovery, refresh, and rebinding, or reject secure view creation before any mutation. Never silently reconstruct it as REGULAR_VIEW." + ], + "area": "views", + "disposition": "include", + "target": "catalog_view_create.security / ViewInfo.security", + "status": "contract_reviewed_existing_wire_native2_enforcement_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/capabilities.py", + "vgi/worker.py", + "tests/catalog/test_view_security_protocol.py", + "docs/protocol/view-security.md", + "native:src/vgi_catalog_api.cpp", + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "native:docs/duckdb-2.0-catalog-audit.md:265" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "implementation_evidence": "Explicit regular/barrier metadata and CREATE parameter across all SDKs; Python negotiation and durable metadata reference; native1.5 rejects barrier/malformed metadata on get/list/bulk paths. Native2 enforcement and final contract review remain pending; see view_security_validation.", + "decision": "Retain required explicit regular/barrier security. Barrier READ and CREATE remain distinct existing profiles, CREATE requires READ; unsupported readers fail on get/list/bulk/cache paths, never project a barrier to regular. Barrier mutations recheck the current object and transaction guarantee. Native1.5 remains reject-only; native2 must install security_type and prove optimizer/rebind behavior before offering support.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-266", + "source_line": 266, + "section": "Views and comments", + "field": "Inherited comment, tags, dependencies", + "audit_evidence": [ + "Existing", + "See shared fields. Reading these fields from worker metadata does not establish create/update support." + ], + "area": "dependencies", + "disposition": "include", + "target": "catalog_view_create.comment/tags + provider-derived ObjectDependency", + "status": "contract_reviewed_existing_wire_adapter_gaps", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/options.py", + "vgi/catalog/metadata_changes.py", + "vgi/catalog/capabilities.py", + "tests/catalog/test_schema_options_protocol.py", + "tests/catalog/test_schema_metadata_protocol.py", + "docs/protocol/shared-catalog-values.md#attachment-negotiation-and-schema-option-operations", + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "vgi/catalog/layout.py", + "vgi/catalog/options.py", + "vgi/worker.py", + "tests/catalog/test_table_layout_protocol.py", + "tests/catalog/table_layout_reference.py", + "docs/protocol/shared-catalog-values.md#table-options-creation-metadata-and-layout", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:266" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse existing object-specific create comment/tags fields and provider-derived dependencies. NULL/empty comment and omitted/empty maps retain their documented meanings; validate whole create/replacement before effects. Schema/view creation metadata is implemented; table creation fields are present but native forwarding remains pending. Do not add generic MetadataPatch/metadata-set methods for already-covered operations.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-279", + "source_line": 279, + "section": "Views and comments", + "field": "`RenameViewInfo.new_view_name`, `alter_view_type`", + "audit_evidence": [ + "Rename is carried; other view variants reject. Port typed identity and verify dependent view behavior." + ], + "area": "views", + "disposition": "existing_contract", + "target": "catalog_view_rename.name/new_name + provider dependency policy", + "status": "contract_reviewed_existing_wire_provider_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:docs/duckdb-2.0-catalog-audit.md:279" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep existing view rename identity fields and provider authority; invalidate dependent views. Rename with protected/unrewriteable dependencies rejects atomically until a provider has qualified semantic rewrite support. Do not advertise arbitrary view SQL rewriting or add a rename RPC.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-280", + "source_line": 280, + "section": "Views and comments", + "field": "`SetCommentInfo.entry_catalog_type`, `comment_value`", + "audit_evidence": [ + "Correct table/view dispatch; SQL NULL is distinguished from a string. Other object kinds reject, including schema comments through this path." + ], + "area": "dependencies", + "disposition": "existing_contract", + "target": "catalog_table_comment_set / catalog_view_comment_set (schema metadata uses catalog_schema_alter_metadata)", + "status": "contract_reviewed_existing_contract_implemented_bounded", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:280" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse dedicated table/view comment and column-comment RPCs. NULL removes the comment, empty string remains a present comment. Table columns use raw ColumnPath (nested comment profile where applicable); view outputs take exactly one raw identifier. Dispatch by actual table/view kind, validate output existence when the owning binder can do so, and invalidate the correct cache. No generic MetadataPatch/object_metadata_set is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-281", + "source_line": 281, + "section": "Views and comments", + "field": "`SetColumnCommentInfo.catalog_entry_type`", + "audit_evidence": [ + "**Lost dispatch information.** VGI always calls table-column-comment RPC and invalidates the table cache, even though DuckDB resolves table or view. Add view-column support or reject view targets explicitly." + ], + "area": "views", + "disposition": "include", + "target": "Native table/view discriminator -> dedicated catalog_*_column_comment_set RPC", + "status": "contract_reviewed_existing_contract_implemented_bounded", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:281" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse dedicated table/view comment and column-comment RPCs. NULL removes the comment, empty string remains a present comment. Table columns use raw ColumnPath (nested comment profile where applicable); view outputs take exactly one raw identifier. Dispatch by actual table/view kind, validate output existence when the owning binder can do so, and invalidate the correct cache. No generic MetadataPatch/object_metadata_set is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-282", + "source_line": 282, + "section": "Views and comments", + "field": "`SetColumnCommentInfo.column_name`, `comment_value`", + "audit_evidence": [ + "Carried for table columns, including null comment removal. Port `Identifier`; do not invent nested comment syntax beyond what the target resolves." + ], + "area": "views", + "disposition": "include", + "target": "Existing column_path/comment fields; view path has exactly one component", + "status": "contract_reviewed_existing_contract_implemented_bounded", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/catalog_interface.py", + "vgi/protocol.py", + "tests/catalog/test_view_metadata_protocol.py", + "tests/catalog/test_view_metadata_cross_sdk.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_view_set.cpp", + "docs/protocol/view-metadata.md", + "vgi/column_path.py", + "vgi/protocol.py", + "tests/catalog/test_column_paths_protocol.py", + "tests/catalog/column_paths_reference.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/vgi_arrow_utils.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:282" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse dedicated table/view comment and column-comment RPCs. NULL removes the comment, empty string remains a present comment. Table columns use raw ColumnPath (nested comment profile where applicable); view outputs take exactly one raw identifier. Dispatch by actual table/view kind, validate output existence when the owning binder can do so, and invalidate the correct cache. No generic MetadataPatch/object_metadata_set is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-283", + "source_line": 283, + "section": "Views and comments", + "field": "`ChangeOwnershipInfo.entry_catalog_type`, `owner_schema`, `owner_name`", + "audit_evidence": [ + "Rejected; no ownership contract." + ], + "area": "dependencies", + "disposition": "defer", + "target": "Explicit unsupported CHANGE OWNERSHIP; no ObjectDependency authorization mapping", + "status": "contract_reviewed_deferred", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "native:src/storage/vgi_schema_entry.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:283" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Defer SQL CHANGE OWNERSHIP. ObjectDependency.kind=owned expresses object lifetime/drop closure, not principal privileges or a SQL owner-transfer operation. No current authorization/owner representation or mutation contract exists; native should continue rejecting before effects.", + "remaining_implementation_or_qualification": [ + "A future owner-transfer feature requires an explicit principal/ownership and authorization model, likely a new capability and operation; include in a later coordinated protocol release only if required." + ] + }, + { + "id": "audit-284", + "source_line": 284, + "section": "Views and comments", + "field": "`ALTER_SEQUENCE`, `ALTER_SCALAR_FUNCTION`, `ALTER_TABLE_FUNCTION`", + "audit_evidence": [ + "No native VGI alteration support. Function registration/execution migration is separate from exposing SQL mutation of worker definitions." + ], + "area": "views", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:284" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "SQL ALTER SEQUENCE/FUNCTION families remain unsupported adapter dispatch unless explicitly implemented. Existing function execution/registration is not SQL mutation authority.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-294", + "source_line": 294, + "section": "DROP fields and object lifecycle", + "field": "`type`", + "audit_evidence": [ + "Native DROP supports schemas, tables, and views. Other kinds require integration even where a wire operation already exists." + ], + "area": "dependencies", + "disposition": "existing_contract", + "target": "Existing object-specific DROP dispatch / CatalogObjectRef.kind", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:294" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep object-specific DROP with raw identity, ignore_not_found and cascade. Missing ignored targets are no-ops with unchanged version/etag. Provider revalidates and authorizes the entire closure inside its transaction; requires blocks RESTRICT and owned auto-drops its dependent. No graph read reserves a later mutation. Native supported kinds are schemas/tables/views/indexes; other kinds require their own qualified adapters.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-295", + "source_line": 295, + "section": "DROP fields and object lifecycle", + "field": "`qualified_name`", + "audit_evidence": [ + "Port full identity; no splitting SQL strings." + ], + "area": "dependencies", + "disposition": "existing_contract", + "target": "CatalogObjectRef.schema_path/name", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/schema_path.py", + "vgi/catalog/dependencies.py", + "tests/test_schema_paths_v2.py", + "native:src/vgi_rpc_types.cpp", + "native:src/storage/vgi_schema_set.cpp", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:295" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Use existing attachment-relative raw SchemaPath/name or CatalogObjectRef fields. Schema objects use parents plus leaf; table-owned constraints use owner_table. Never split dotted names, send aliases/OIDs, or silently flatten nested ownership. No additional object-identity RPC is required.", + "remaining_implementation_or_qualification": [ + "Native2 schema-tree/typed-identifier/recursive lookup port and nested lifecycle qualification remain open." + ] + }, + { + "id": "audit-296", + "source_line": 296, + "section": "DROP fields and object lifecycle", + "field": "`if_not_found`", + "audit_evidence": [ + "Carried as `ignore_not_found` for supported drops. Test both local absence and stale local metadata." + ], + "area": "dependencies", + "disposition": "existing_contract", + "target": "Existing drop.ignore_not_found", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:296" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep object-specific DROP with raw identity, ignore_not_found and cascade. Missing ignored targets are no-ops with unchanged version/etag. Provider revalidates and authorizes the entire closure inside its transaction; requires blocks RESTRICT and owned auto-drops its dependent. No graph read reserves a later mutation. Native supported kinds are schemas/tables/views/indexes; other kinds require their own qualified adapters.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-297", + "source_line": 297, + "section": "DROP fields and object lifecycle", + "field": "`cascade`", + "audit_evidence": [ + "Carried for schema/table/view drops. Worker action and dependency enforcement are not proven by transporting the boolean." + ], + "area": "dependencies", + "disposition": "existing_contract", + "target": "Existing drop.cascade + provider-authoritative dependency closure", + "status": "contract_reviewed_reference_qualified_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/catalog/dependencies.py", + "vgi/worker.py", + "tests/catalog/dependencies_reference.py", + "tests/catalog/test_dependencies_protocol.py", + "tests/catalog/test_dependency_alter_lifecycle.py", + "native:test/run_semantic_alter.py", + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "native:docs/duckdb-2.0-catalog-audit.md:297" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep object-specific DROP with raw identity, ignore_not_found and cascade. Missing ignored targets are no-ops with unchanged version/etag. Provider revalidates and authorizes the entire closure inside its transaction; requires blocks RESTRICT and owned auto-drops its dependent. No graph read reserves a later mutation. Native supported kinds are schemas/tables/views/indexes; other kinds require their own qualified adapters.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-298", + "source_line": 298, + "section": "DROP fields and object lifecycle", + "field": "`allow_drop_internal`", + "audit_evidence": [ + "Engine access-control field, not a worker privilege switch." + ], + "area": "dependencies", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:298" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "allow_drop_internal is an engine policy flag, not a worker privilege switch.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-299", + "source_line": 299, + "section": "DROP fields and object lifecycle", + "field": "`extra_drop_info` / `info_type`", + "audit_evidence": [ + "Type-specific dispatch. New trigger support needs the trigger form below; do not discard the discriminator." + ], + "area": "dependencies", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:299" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Keep type-specific native DROP dispatch; reject unknown extra-drop forms. Trigger DROP remains deferred with trigger implementation, not silently sent as an ordinary table/view DROP.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-300", + "source_line": 300, + "section": "DROP fields and object lifecycle", + "field": "`ExtraDropTriggerInfo.base_table`", + "audit_evidence": [ + "Resolve trigger ownership to its target table, then use trigger hooks/storage. No VGI lifecycle implementation today." + ], + "area": "triggers", + "disposition": "existing_contract_plus_engine_port", + "target": "CatalogObjectRef(kind=trigger).owner_table", + "status": "reviewed_existing_or_engine_mapping_qualification_pending", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "CatalogObjectRef trigger owner already represents ExtraDropTriggerInfo.base_table; native trigger DROP hook/RPC implementation is deferred. This closes identity representation only." + }, + { + "id": "audit-301", + "source_line": 301, + "section": "DROP fields and object lifecycle", + "field": "`ExtraDropSecretInfo.persist_mode`, `secret_storage`", + "audit_evidence": [ + "Secret-manager path, outside ordinary VGI schema DDL. Preserve the existing remote-secret integration in its own port; these are not table/schema RPC omissions." + ], + "area": "dependencies", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:301" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Secret persistence/storage belongs to the existing secret-manager integration and its native2 port, not ordinary catalog DDL.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-323", + "source_line": 323, + "section": "Index fields", + "field": "Index `qualified_name`, `table`", + "audit_evidence": [ + "Native CREATE INDEX rejected. Wire already has `schema_path`, `name`, `table_name`; port identity and integrate binding/create/drop." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "IndexCreateRequest identity / IndexDefinition.table", + "status": "implemented_wire_bounded_native15_qualification", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "typed_indexes_public_reference_and_native15_bounded", + "sources": [ + "vgi/catalog/indexes.py", + "vgi/protocol.py", + "vgi/worker.py", + "tests/catalog/index_protocol_reference.py", + "tests/catalog/test_index_public_contract.py", + "tests/catalog/test_index_public_lifecycle.py", + "tests/catalog/test_index_example_cross_sdk.py", + "conformance/catalog-contracts.json" + ], + "limits": "Native/reference regular and UNIQUE raw top-level keys with binary collation and explicit SQLITE.BTREE.V1. Rich typed options, expression/nested keys and PRIMARY association have wire coverage but no provider qualification. No constraint-backing closure, CASCADE, general ALTER dependency lifecycle or DuckDB2 runtime proof." + } + ] + }, + { + "id": "audit-324", + "source_line": 324, + "section": "Index fields", + "field": "`index_type`", + "audit_evidence": [ + "Wire already has a string field. Negotiate supported algorithms; do not promise ART behavior for an unrelated remote backend." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "IndexDefinition.algorithm", + "status": "implemented_wire_bounded_native15_qualification", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "typed_indexes_public_reference_and_native15_bounded", + "sources": [ + "vgi/catalog/indexes.py", + "vgi/protocol.py", + "vgi/worker.py", + "tests/catalog/index_protocol_reference.py", + "tests/catalog/test_index_public_contract.py", + "tests/catalog/test_index_public_lifecycle.py", + "tests/catalog/test_index_example_cross_sdk.py", + "conformance/catalog-contracts.json" + ], + "limits": "Native/reference regular and UNIQUE raw top-level keys with binary collation and explicit SQLITE.BTREE.V1. Rich typed options, expression/nested keys and PRIMARY association have wire coverage but no provider qualification. No constraint-backing closure, CASCADE, general ALTER dependency lifecycle or DuckDB2 runtime proof." + } + ] + }, + { + "id": "audit-325", + "source_line": 325, + "section": "Index fields", + "field": "`constraint_type`", + "audit_evidence": [ + "Wire already represents NONE/UNIQUE/PRIMARY. Enforcement must be verified." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "IndexDefinition.constraint_type/associated_constraint", + "status": "implemented_wire_bounded_native15_qualification", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "typed_indexes_public_reference_and_native15_bounded", + "sources": [ + "vgi/catalog/indexes.py", + "vgi/protocol.py", + "vgi/worker.py", + "tests/catalog/index_protocol_reference.py", + "tests/catalog/test_index_public_contract.py", + "tests/catalog/test_index_public_lifecycle.py", + "tests/catalog/test_index_example_cross_sdk.py", + "conformance/catalog-contracts.json" + ], + "limits": "Native/reference regular and UNIQUE raw top-level keys with binary collation and explicit SQLITE.BTREE.V1. Rich typed options, expression/nested keys and PRIMARY association have wire coverage but no provider qualification. No constraint-backing closure, CASCADE, general ALTER dependency lifecycle or DuckDB2 runtime proof." + } + ] + }, + { + "id": "audit-326", + "source_line": 326, + "section": "Index fields", + "field": "`options`", + "audit_evidence": [ + "Engine values are typed `Value`; wire currently uses `dict`. Define lossless supported value encoding or typed protocol extension before enabling arbitrary WITH options." + ], + "area": "constraints", + "disposition": "include", + "target": "IndexDefinition.options: TypedValueBatch / OptionDefinitions / OptionState", + "status": "implemented_wire_bounded_native15_qualification", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "typed_indexes_public_reference_and_native15_bounded", + "sources": [ + "vgi/catalog/indexes.py", + "vgi/protocol.py", + "vgi/worker.py", + "tests/catalog/index_protocol_reference.py", + "tests/catalog/test_index_public_contract.py", + "tests/catalog/test_index_public_lifecycle.py", + "tests/catalog/test_index_example_cross_sdk.py", + "conformance/catalog-contracts.json" + ], + "limits": "Native/reference regular and UNIQUE raw top-level keys with binary collation and explicit SQLITE.BTREE.V1. Rich typed options, expression/nested keys and PRIMARY association have wire coverage but no provider qualification. No constraint-backing closure, CASCADE, general ALTER dependency lifecycle or DuckDB2 runtime proof." + } + ] + }, + { + "id": "audit-327", + "source_line": 327, + "section": "Index fields", + "field": "`expressions`, `parsed_expressions`", + "audit_evidence": [ + "Wire already accepts SQL expression strings. Select the correct original/bound representation and preserve names and qualification." + ], + "area": "constraints", + "disposition": "existing_contract", + "target": "IndexDefinition.keys: ColumnPath or SqlExpression", + "status": "implemented_wire_bounded_native15_qualification", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#canonical-constraints-and-index-definitions" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified." + }, + { + "status": "typed_indexes_public_reference_and_native15_bounded", + "sources": [ + "vgi/catalog/indexes.py", + "vgi/protocol.py", + "vgi/worker.py", + "tests/catalog/index_protocol_reference.py", + "tests/catalog/test_index_public_contract.py", + "tests/catalog/test_index_public_lifecycle.py", + "tests/catalog/test_index_example_cross_sdk.py", + "conformance/catalog-contracts.json" + ], + "limits": "Native/reference regular and UNIQUE raw top-level keys with binary collation and explicit SQLITE.BTREE.V1. Rich typed options, expression/nested keys and PRIMARY association have wire coverage but no provider qualification. No constraint-backing closure, CASCADE, general ALTER dependency lifecycle or DuckDB2 runtime proof." + } + ] + }, + { + "id": "audit-328", + "source_line": 328, + "section": "Index fields", + "field": "`column_ids`, `scan_types`, `names`", + "audit_evidence": [ + "Engine index build/scan data. Adapt binding/planning locally; these are not inherently missing remote index-definition fields." + ], + "area": "constraints", + "disposition": "engine_only", + "target": null, + "status": "pending_native_qualification", + "evidence": [] + }, + { + "id": "audit-343", + "source_line": 343, + "section": "Macro and function creation fields", + "field": "`CreateFunctionInfo.qualified_name`", + "audit_evidence": [ + "Discovered worker scalar/aggregate/table functions and macros are installed locally. SQL CREATE through `VgiSchemaEntry::CreateFunction` rejects. Preserve this distinction during the API port." + ], + "area": "macros", + "disposition": "existing_contract_plus_engine_port", + "target": "CatalogSchemaObject.schema_path/name and MacroCreateRequest owner/name; CatalogObjectRef where dependencies need a kind", + "status": "reviewed_existing_identity_native_port_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/catalog_interface.py:648,791", + "native:src/storage/vgi_schema_entry.cpp:333", + "haybarn:src/include/duckdb/parser/parsed_data/create_function_info.hpp:37" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + } + ], + "decision": "Raw qualified object identity already exists. Do not add a general CREATE FUNCTION RPC merely because CreateFunctionInfo uses QualifiedName. Native discovery registration and native SQL CREATE are separate paths; CreateFunction currently rejects." + }, + { + "id": "audit-344", + "source_line": 344, + "section": "Macro and function creation fields", + "field": "`alias_of`", + "audit_evidence": [ + "Function metadata/registration field; no general native SQL mutation path. Qualify local aliases during registration port." + ], + "area": "macros", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_only_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "haybarn:src/include/duckdb/parser/parsed_data/create_function_info.hpp:32", + "native:src/vgi_extension.cpp:1423", + "native:src/storage/vgi_schema_entry.cpp:333" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + } + ], + "decision": "alias_of is local registration provenance. Existing separately named discovery entries can register callable aliases; no requested remote alias-creation semantics justify a new field. Qualify local catalog alias/name handling during native port." + }, + { + "id": "audit-345", + "source_line": 345, + "section": "Macro and function creation fields", + "field": "`descriptions`: `parameter_types`, `parameter_names`, `description`, `examples`, `categories`", + "audit_evidence": [ + "Local descriptive/signature data. Existing discovery has documentation channels; do not equate these structures with an implemented worker CREATE FUNCTION request." + ], + "area": "macros", + "disposition": "existing_contract_plus_engine_port", + "target": "FunctionInfo.arguments/description/examples/categories; MacroParameter.name/declared_type/documentation", + "status": "reviewed_existing_documentation_native_port_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/catalog_interface.py:908,918", + "python:vgi/catalog/macros.py:83", + "native:src/storage/vgi_scalar_function_set.cpp:140", + "haybarn:src/include/duckdb/parser/parsed_data/create_function_info.hpp:16" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + } + ], + "decision": "Correct the manifest's macro-only target: discovered scalar/aggregate/table FunctionInfo already carries every FunctionDescription category and native registration maps them. Macro per-parameter docs are separate. Do not claim arbitrary inherited macro description vectors round-trip; SQL macro body/signature/comments are the bounded promise and there is no new SQL description grammar requiring another generic record." + }, + { + "id": "audit-346", + "source_line": 346, + "section": "Macro and function creation fields", + "field": "`CreateMacroInfo.macros`", + "audit_evidence": [ + "Vector of overloads. Native creation rejected, although macro-create/get/list/drop RPCs exist. Current `MacroCreateRequest` represents one definition, not this whole overload vector." + ], + "area": "macros", + "disposition": "include", + "target": "MacroInfo.overloads / MacroCreateRequest.overloads", + "status": "canonical_family_bounded_native15_implemented_and_validated", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/catalog_interface.py:667,679", + "python:vgi/catalog/macros.py:189", + "python:docs/protocol/macro-contract.md:90,116", + "native:src/vgi_catalog_api.cpp:3003", + "haybarn:src/include/duckdb/parser/parsed_data/create_macro_info.hpp:19" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + }, + { + "status": "canonical_macro_family_implementation", + "checkpoint": "macro_family_checkpoint_2026_10_23", + "sources": [ + "python:docs/protocol/macro-contract.md", + "native:src/vgi_macro_contract.cpp", + "native:src/storage/vgi_macro_set.cpp" + ], + "limits": "Bounded owner context and native1.5 discovery/execution only; native SQL CREATE, general dependency extraction and native2 remain open." + } + ], + "decision": "The original single-definition claim is historical. Required canonical families now replace duplicate parent wire forms, and native1.5 materializes the whole vector. Existing catalog create/get/list/drop RPCs retain family identity; native SQL CREATE remains unsupported." + }, + { + "id": "audit-347", + "source_line": 347, + "section": "Macro and function creation fields", + "field": "Each macro's `type`, `parameters`", + "audit_evidence": [ + "Wire has `macro_type` and ordered parameter names. Define accepted parameter forms and scalar/table distinction." + ], + "area": "macros", + "disposition": "existing_contract_plus_engine_port", + "target": "MacroOverload.macro_type/parameters and MacroParameter.name", + "status": "canonical_family_bounded_native15_implemented_and_validated", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/macros.py:128,137,144,189", + "python:tests/catalog/test_macro_contract.py:132,170", + "haybarn:src/include/duckdb/function/macro_function.hpp:38" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + }, + { + "status": "canonical_macro_family_implementation", + "checkpoint": "macro_family_checkpoint_2026_10_23", + "sources": [ + "python:docs/protocol/macro-contract.md", + "native:src/vgi_macro_contract.cpp", + "native:src/storage/vgi_macro_set.cpp" + ], + "limits": "Bounded owner context and native1.5 discovery/execution only; native SQL CREATE, general dependency extraction and native2 remain open." + } + ], + "decision": "Canonical scalar/table families preserve ordered raw parameters, uniqueness, required/default ordering and parent agreement. Native1.5 materializes complete vectors and delegates overload selection to the pinned engine; broader contexts remain unqualified." + }, + { + "id": "audit-348", + "source_line": 348, + "section": "Macro and function creation fields", + "field": "`default_parameters`", + "audit_evidence": [ + "Engine stores expressions; wire carries typed values in a one-row batch. Define which default expressions can be represented; do not evaluate arbitrary expressions merely to force them into value fields." + ], + "area": "macros", + "disposition": "include", + "target": "MacroParameter.default_expression/default_value with CREATE-time resolution", + "status": "canonical_family_bounded_native15_implemented_and_validated", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified.", + "historical": true, + "superseded_by": "combined_protocol_review_2026_10_23" + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/macros.py:83,320", + "python:docs/protocol/macro-contract.md:99", + "python:tests/catalog/test_macro_contract.py:288,327", + "haybarn:src/planner/binder/statement/bind_create.cpp:476,492,501,503" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + }, + { + "status": "canonical_macro_family_implementation", + "checkpoint": "macro_family_checkpoint_2026_10_23", + "sources": [ + "python:docs/protocol/macro-contract.md", + "native:src/vgi_macro_contract.cpp", + "native:src/storage/vgi_macro_set.cpp" + ], + "limits": "Bounded owner context and native1.5 discovery/execution only; native SQL CREATE, general dependency extraction and native2 remain open." + } + ], + "decision": "Defaults resolve at CREATE through the owner; discovery accepts resolved literal batches only. Native1.5 imports Arrow literals through the existing scanner, preserving typed NULL/default values instead of string fallback. Finite durable owner proof is separate from general authorization/binder qualification." + }, + { + "id": "audit-349", + "source_line": 349, + "section": "Macro and function creation fields", + "field": "`types`", + "audit_evidence": [ + "Typed macro parameters need a semantic mapping. Wire `arguments_schema` currently describes parameter/default types and docs; qualify that it can express the desired declared-type contract." + ], + "area": "macros", + "disposition": "existing_contract_plus_engine_port", + "target": "MacroParameter.declared_type", + "status": "canonical_family_bounded_native15_implemented_and_validated", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/macros.py:74,92,218", + "python:docs/protocol/macro-contract.md:61,66,90", + "python:tests/catalog/test_macro_contract.py:50,182,339", + "haybarn:src/planner/binder/statement/bind_create.cpp:510" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + }, + { + "status": "canonical_macro_family_implementation", + "checkpoint": "macro_family_checkpoint_2026_10_23", + "sources": [ + "python:docs/protocol/macro-contract.md", + "native:src/vgi_macro_contract.cpp", + "native:src/storage/vgi_macro_set.cpp" + ], + "limits": "Bounded owner context and native1.5 discovery/execution only; native SQL CREATE, general dependency extraction and native2 remain open." + } + ], + "decision": "Singleton Arrow declared type is distinct from literal default type and documentation. Never infer declarations from legacy arguments_schema. Owner resolves nominal aliases and overload costs; generic codec does not. Coordinate named logical type identity with that separate area rather than adding macro-specific type IDs." + }, + { + "id": "audit-350", + "source_line": 350, + "section": "Macro and function creation fields", + "field": "`ScalarMacroFunction.expression`, `TableMacroFunction.query_node`", + "audit_evidence": [ + "Wire has SQL `definition`; preserve owner-relative name resolution, overload selection, and dependencies." + ], + "area": "macros", + "disposition": "existing_contract_plus_engine_port", + "target": "MacroOverload.definition/macro_type/profile/schema_path/evaluation_context plus existing dependency graph", + "status": "canonical_family_bounded_native15_implemented_and_validated", + "evidence": [ + { + "status": "specified_reference_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases" + ], + "limits": "Design mapping reviewed against pinned source; new wire fields and semantic behavior remain unimplemented/unqualified.", + "historical": true, + "superseded_by": "combined_protocol_review_2026_10_23" + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/macros.py:137", + "python:docs/protocol/macro-contract.md:83,106,116", + "native:src/storage/vgi_macro_set.cpp:31,140,153", + "native:src/vgi_catalog_api.cpp:3003" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + }, + { + "status": "canonical_macro_family_implementation", + "checkpoint": "macro_family_checkpoint_2026_10_23", + "sources": [ + "python:docs/protocol/macro-contract.md", + "native:src/vgi_macro_contract.cpp", + "native:src/storage/vgi_macro_set.cpp" + ], + "limits": "Bounded owner context and native1.5 discovery/execution only; native SQL CREATE, general dependency extraction and native2 remain open." + } + ], + "decision": "Owner/context/body fields are mandatory. Bounded native1.5 bodies are parsed and owner-qualified before family publication, then bind at invocation. Unsupported query forms, catalog qualification and contexts reject. General dependency extraction and full owner-relative body/type binding remain open; no serialized AST or new lifecycle." + }, + { + "id": "audit-362", + "source_line": 362, + "section": "Other creation structures", + "field": "`CreateSequenceInfo`: name, `usage_count`, `increment`, `min_value`, `max_value`, `start_value`, `cycle`, `last_value`", + "audit_evidence": [ + "Native creation rejected; no sequence lifecycle/state RPC. A sequence feature needs allocation, concurrency, rollback policy, and metadata semantics, not just numeric field transport." + ], + "area": "sequences_types", + "disposition": "defer", + "target": "Deferred feature: `CreateSequenceInfo`: name, `usage_count`, `increment`, `min_value`, `max_value`, `start_value`, `cycle`, `last_value`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:src/storage/vgi_schema_entry.cpp:408-432 rejects sequence, type and collation CREATE.", + "engine@pin:src/include/duckdb/catalog/catalog_entry/sequence_catalog_entry.hpp:62-74 nonvirtual operations and private state.", + "engine@pin:src/function/scalar/sequence/nextval.cpp:72-81 obtains DuckTransaction for the sequence catalog.", + "engine:src/include/duckdb/parser/parsed_data/create_type_info.hpp:19-36 type/query/constructors.", + "engine:src/planner/binder/statement/bind_create.cpp:970-1010 distinguishes resolved type and query-derived enum.", + "native:src/vgi_arrow_ipc.cpp:527-532 resolves dictionary type to its value type; Arrow dictionary encoding is not an enum domain contract.", + "python:vgi/catalog/dependencies.py:80-105 already accepts sequence/type identity kinds." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Sequence allocation/state authority and nonvirtual DuckTransaction-coupled nextval need a real adapter plus concurrency/rollback proof; no speculative SequenceInfo/Allocation wire." + }, + { + "id": "audit-363", + "source_line": 363, + "section": "Other creation structures", + "field": "`CreateTypeInfo`: name, `type`, `query`, `constructors`", + "audit_evidence": [ + "Native creation rejected. 2.0 constructor representation differs from 1.5 bind-function registration. User type/enum creation and discovery require an explicit contract; opaque C++ constructor callbacks cannot simply cross the wire." + ], + "area": "sequences_types", + "disposition": "defer", + "target": "Deferred feature: `CreateTypeInfo`: name, `type`, `query`, `constructors`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:src/storage/vgi_schema_entry.cpp:408-432 rejects sequence, type and collation CREATE.", + "engine@pin:src/include/duckdb/catalog/catalog_entry/sequence_catalog_entry.hpp:62-74 nonvirtual operations and private state.", + "engine@pin:src/function/scalar/sequence/nextval.cpp:72-81 obtains DuckTransaction for the sequence catalog.", + "engine:src/include/duckdb/parser/parsed_data/create_type_info.hpp:19-36 type/query/constructors.", + "engine:src/planner/binder/statement/bind_create.cpp:970-1010 distinguishes resolved type and query-derived enum.", + "native:src/vgi_arrow_ipc.cpp:527-532 resolves dictionary type to its value type; Arrow dictionary encoding is not an enum domain contract.", + "python:vgi/catalog/dependencies.py:80-105 already accepts sequence/type identity kinds." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Named alias/ENUM identity/domain preservation is not resolved storage transport. Defer catalog CRUD; constructors are engine-only, query-derived enum can be normalized by the engine if later included." + }, + { + "id": "audit-364", + "source_line": 364, + "section": "Other creation structures", + "field": "`CreateCollationInfo`: name, `function`, `combinable`, `not_required_for_equality`", + "audit_evidence": [ + "Native creation rejected. Collation execution and comparison semantics need engine-local integration; no generic callback serialization." + ], + "area": "collations_coordinates", + "disposition": "engine_only", + "target": null, + "status": "reviewed_existing_or_engine_mapping_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "engine:src/include/duckdb/parser/parsed_data/create_collation_info.hpp:28-35 contains ScalarFunction and semantic flags.", + "engine:src/include/duckdb/parser/parsed_data/create_coordinate_system_info.hpp:29-39 contains portable authority/code/PROJJSON/WKT strings; structure also exists on the 1.5 baseline.", + "engine:src/function/table/system/duckdb_coordinate_systems.cpp:59-66 enumerates actual COORDINATE_SYSTEM_ENTRY objects.", + "python:vgi/catalog/query.py:129-155 carries an explicit QueryEvaluationContext; docs/protocol/vgi-catalog-query-pushdown-design.md:325-335 requires certification beyond binary/UTC.", + "python:vgi/catalog/dependencies.py:80-95 has no collation or coordinate-system identity kind." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Collation callbacks/composition/equality shortcut require a qualified engine implementation; existing context can name only certified semantics. Remote collation catalog discovery is deferred." + }, + { + "id": "audit-365", + "source_line": 365, + "section": "Other creation structures", + "field": "`CreateCoordinateSystemInfo`: name, `authority`, `code`, `projjson_definition`, `wkt2_2019_definition`", + "audit_evidence": [ + "No VGI creation/discovery integration. This structure already exists in the 1.5 baseline; it is not solely a new 2.0 gap." + ], + "area": "collations_coordinates", + "disposition": "defer", + "target": "Deferred feature: `CreateCoordinateSystemInfo`: name, `authority`, `code`, `projjson_definition`, `wkt2_2019_definition`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "engine:src/include/duckdb/parser/parsed_data/create_collation_info.hpp:28-35 contains ScalarFunction and semantic flags.", + "engine:src/include/duckdb/parser/parsed_data/create_coordinate_system_info.hpp:29-39 contains portable authority/code/PROJJSON/WKT strings; structure also exists on the 1.5 baseline.", + "engine:src/function/table/system/duckdb_coordinate_systems.cpp:59-66 enumerates actual COORDINATE_SYSTEM_ENTRY objects.", + "python:vgi/catalog/query.py:129-155 carries an explicit QueryEvaluationContext; docs/protocol/vgi-catalog-query-pushdown-design.md:325-335 requires certification beyond binary/UTC.", + "python:vgi/catalog/dependencies.py:80-95 has no collation or coordinate-system identity kind." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Portable CRS descriptions are not currently needed as a new catalog object API; preserve existing field metadata without claiming resolver/transformation semantics." + }, + { + "id": "audit-366", + "source_line": 366, + "section": "Other creation structures", + "field": "`CreateTableFunctionInfo`, `CreateCopyFunctionInfo`, `CreatePragmaFunctionInfo` callable definitions", + "audit_evidence": [ + "Native schema creation hooks reject. Existing extension registration and worker function discovery are separate supported paths; port their callbacks in the function/COPY workstream." + ], + "area": "sequences_types", + "disposition": "engine_only", + "target": null, + "status": "reviewed_existing_or_engine_mapping_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:src/storage/vgi_schema_entry.cpp:408-432 rejects sequence, type and collation CREATE.", + "engine@pin:src/include/duckdb/catalog/catalog_entry/sequence_catalog_entry.hpp:62-74 nonvirtual operations and private state.", + "engine@pin:src/function/scalar/sequence/nextval.cpp:72-81 obtains DuckTransaction for the sequence catalog.", + "engine:src/include/duckdb/parser/parsed_data/create_type_info.hpp:19-36 type/query/constructors.", + "engine:src/planner/binder/statement/bind_create.cpp:970-1010 distinguishes resolved type and query-derived enum.", + "native:src/vgi_arrow_ipc.cpp:527-532 resolves dictionary type to its value type; Arrow dictionary encoding is not an enum domain contract.", + "python:vgi/catalog/dependencies.py:80-105 already accepts sequence/type identity kinds." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Callable creation hooks are engine registration; existing worker function/COPY discovery stays. Do not serialize native callbacks." + }, + { + "id": "audit-381", + "source_line": 381, + "section": "Trigger fields", + "field": "Inherited `qualified_name`, conflict/lifecycle/metadata fields", + "audit_evidence": [ + "Define identity, replacement, persistence, and dependencies." + ], + "area": "triggers", + "disposition": "defer", + "target": "Deferred feature: Inherited `qualified_name`, conflict/lifecycle/metadata fields; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Persistent trigger lifecycle/conflict/comment/tag metadata could reuse existing carriers, but new authoritative CRUD is deferred pending product need and ownership proof." + }, + { + "id": "audit-382", + "source_line": 382, + "section": "Trigger fields", + "field": "`base_table`", + "audit_evidence": [ + "Resolve full table ownership, including nested schema paths." + ], + "area": "triggers", + "disposition": "existing_contract_plus_engine_port", + "target": "CatalogObjectRef(kind=trigger).owner_table", + "status": "reviewed_existing_or_engine_mapping_qualification_pending", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Table-owned raw identity is implemented; does not imply native table trigger hooks or executable support." + }, + { + "id": "audit-383", + "source_line": 383, + "section": "Trigger fields", + "field": "`timing`", + "audit_evidence": [ + "Implement the advertised before/after behavior and failure ordering." + ], + "area": "triggers", + "disposition": "defer", + "target": "Deferred feature: `timing`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "BEFORE/AFTER is execution behavior; freeze with bounded trigger execution proof rather than an unused timing enum." + }, + { + "id": "audit-384", + "source_line": 384, + "section": "Trigger fields", + "field": "`event_type`", + "audit_evidence": [ + "Select the correct write events; qualify INSERT/UPDATE/DELETE and any combined write plans." + ], + "area": "triggers", + "disposition": "defer", + "target": "Deferred feature: `event_type`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "One event per pinned definition; event CRUD/engine execution deferred; do not add event lists speculatively." + }, + { + "id": "audit-385", + "source_line": 385, + "section": "Trigger fields", + "field": "`columns`", + "audit_evidence": [ + "Preserve UPDATE OF column filtering; typed names must resolve to the target table." + ], + "area": "triggers", + "disposition": "defer", + "target": "Deferred feature: `columns`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Existing raw ColumnPath carrier is sufficient but UPDATE OF mentioned-column semantics and pinned one-component restriction belong to deferred trigger profile." + }, + { + "id": "audit-386", + "source_line": 386, + "section": "Trigger fields", + "field": "`for_each`", + "audit_evidence": [ + "Preserve row versus statement execution count." + ], + "area": "triggers", + "disposition": "defer", + "target": "Deferred feature: `for_each`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Statement versus row firing cannot be inferred from a batch; requires qualified engine semantics, not generic granularity flag." + }, + { + "id": "audit-387", + "source_line": 387, + "section": "Trigger fields", + "field": "`referencing_new_table`, `referencing_old_table`", + "audit_evidence": [ + "Bind transition-table names to the correct NEW/OLD data." + ], + "area": "triggers", + "disposition": "defer", + "target": "Deferred feature: `referencing_new_table`, `referencing_old_table`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "Aliases are ordinary raw identifiers, existing changes carrier supplies OLD/NEW. Native transition mapping/applicability is unqualified and remains with deferred trigger profile." + }, + { + "id": "audit-388", + "source_line": 388, + "section": "Trigger fields", + "field": "`trigger_action`", + "audit_evidence": [ + "Preserve query semantics and dependencies; establish execution location, transaction scope, and recursion policy." + ], + "area": "triggers", + "disposition": "defer", + "target": "Deferred feature: `trigger_action`; no active successor RPC/record selected", + "status": "reviewed_deferred", + "evidence": [ + { + "date": "2026-10-15", + "status": "minimal_trigger_mapping_review_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#trigger-definitions-and-statement-ownership" + ], + "limits": "Preserve this pinned field requirement while reviewing minimal metadata, execution ownership and stale-bind checks against existing write paths. Candidate record names are not finalized and generic statement lifecycle machinery is outside the package." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/dependencies.py:64-105 already supports table-owned trigger identity.", + "python:vgi/catalog/sql.py:62-72 already carries SqlStatement; vgi/write_results.py carries rows/changes separately from input batches.", + "python:docs/protocol/shared-catalog-values.md:1290-1313 describes proposed persistent CRUD but no implemented trigger RPCs.", + "python:docs/protocol/shared-catalog-values.md:1336-1360 identifies execution-owner/discovery and OLD/NEW adaptation gaps.", + "python:docs/protocol/shared-catalog-values.md:1362-1388 reuses transaction/write phases and expressly withdraws general statement/recovery machinery.", + "engine:src/include/duckdb/parser/parsed_data/create_trigger_info.hpp:29-44 has one event, flat UPDATE OF names, transition aliases and action QueryNode.", + "engine:src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:161-174 exposes actual custom-table create/scan/get/drop/event hooks." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + } + ], + "decision": "SqlStatement already represents action text/profile/context; no new carrier required now. Execution owner, allowed grammar and trigger dependency/rollback behavior deferred." + }, + { + "id": "audit-404", + "source_line": 404, + "section": "Attachment and capability boundaries", + "field": "`AttachInfo.name`", + "audit_evidence": [ + "2.0 `Identifier`; engine attachment alias. Keep distinct from the remote catalog name in `path`." + ], + "area": "capabilities", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:404" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Attachment alias is engine-local and distinct from the remote catalog selector; no new remote alias field.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-405", + "source_line": 405, + "section": "Attachment and capability boundaries", + "field": "`path`", + "audit_evidence": [ + "VGI interprets it as remote catalog selection/connection input. Preserve existing parsing, redaction, and cache identity." + ], + "area": "capabilities", + "disposition": "existing_contract", + "target": "catalog_attach.name (remote catalog selector)", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "vgi/attach_capabilities.py", + "vgi/worker.py", + "tests/catalog/test_attach_option.py", + "native:src/vgi_extension.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:405" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain existing catalog_attach remote name/typed options; engine attachment alias stays local. Evaluate ATTACH expressions locally, preserve typed Arrow values/metadata and secret redaction, and include resolved provider/data/implementation/auth identity in cache keys. No AST or external-resource lifecycle record is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-406", + "source_line": 406, + "section": "Attachment and capability boundaries", + "field": "`parsed_path`", + "audit_evidence": [ + "New expression form; engine binds it into the path. Qualify prepared/dynamic attach rather than sending an AST to the worker." + ], + "area": "capabilities", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:406" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "The engine evaluates parsed_path into the existing remote catalog/connection input. Do not transmit an AST; qualify prepared/dynamic ATTACH in native2.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-407", + "source_line": 407, + "section": "Attachment and capability boundaries", + "field": "`parsed_options`, `options`", + "audit_evidence": [ + "Engine binds expressions into values; VGI consumes extension options and validates/forwards declared worker options. Preserve typed values and secret handling through the signature port." + ], + "area": "capabilities", + "disposition": "existing_contract", + "target": "catalog_attach.options / typed Arguments + declared attach option schemas", + "status": "contract_reviewed_existing_wire_native2_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "vgi/attach_capabilities.py", + "vgi/worker.py", + "tests/catalog/test_attach_option.py", + "native:src/vgi_extension.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:407" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Retain existing catalog_attach remote name/typed options; engine attachment alias stays local. Evaluate ATTACH expressions locally, preserve typed Arrow values/metadata and secret redaction, and include resolved provider/data/implementation/auth identity in cache keys. No AST or external-resource lifecycle record is needed.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-408", + "source_line": 408, + "section": "Attachment and capability boundaries", + "field": "`on_conflict`", + "audit_evidence": [ + "Engine manages attachment conflicts. Test IF NOT EXISTS/REPLACE and resource cleanup; do not substitute table-create conflict handling." + ], + "area": "capabilities", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:408" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "ATTACH conflict policy remains DatabaseManager-owned. Qualify IF NOT EXISTS/REPLACE resource cleanup separately from table/schema conflict handling.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-409", + "source_line": 409, + "section": "Attachment and capability boundaries", + "field": "`external_resource`", + "audit_evidence": [ + "New engine resource path. Provisioning/borrowing is handled before attachment, which receives the applied endpoint/options. Not automatically a missing catalog RPC. Qualify resource output compatibility with VGI LOCATION/catalog selection." + ], + "area": "capabilities", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:409" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "External resource provisioning is engine-owned; VGI receives the resolved endpoint/options. No new remote catalog provisioning RPC is justified.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-410", + "source_line": 410, + "section": "Attachment and capability boundaries", + "field": "External resource `parsed_params`, `params`, `provider`, `reference_name`", + "audit_evidence": [ + "Engine binds parameters, provisions or borrows the provider, and owns teardown rules. Test failed attach, DETACH, and borrowed-resource lifetime; do not duplicate provider ownership in the worker." + ], + "area": "capabilities", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:410" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Provider/borrowed resource lifecycle and bound parameters remain engine-owned. Qualify failed ATTACH/DETACH teardown and borrowed lifetime without duplicating ownership in VGI.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-411", + "source_line": 411, + "section": "Attachment and capability boundaries", + "field": "`RenameDatabaseInfo.new_name` and target name", + "audit_evidence": [ + "Engine DatabaseManager renames the attachment. No remote catalog rename RPC is inherently needed. Qualify VGI cached names, published global functions, owner-qualified definitions, companion attachments, and detach cleanup." + ], + "area": "capabilities", + "disposition": "engine_only", + "target": null, + "status": "reviewed_engine_mapping_native_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "native:docs/duckdb-2.0-catalog-audit.md", + "native:src/storage/vgi_schema_entry.cpp", + "native:src/storage/vgi_catalog.cpp", + "native:docs/duckdb-2.0-catalog-audit.md:411" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "DatabaseManager attachment rename is local. No remote catalog rename RPC; audit cache identity, published functions, owner-relative definitions and companion cleanup in native2.", + "remaining_implementation_or_qualification": [] + }, + { + "id": "audit-412", + "source_line": 412, + "section": "Attachment and capability boundaries", + "field": "`SupportsMultipleDMLCTEs()`", + "audit_evidence": [ + "Explicit opt-in. Leave disabled until multiple writes, failure/rollback, and RETURNING share correct worker transaction state. It does not enable ADD COLUMN's rewritten operations by itself." + ], + "area": "transactions", + "disposition": "include", + "target": "Existing transaction/write lifecycle; SupportsMultipleDMLCTEs opt-in remains disabled", + "status": "contract_reviewed_existing_lifecycle_native2_qualification_pending", + "evidence": [ + { + "date": "2026-10-15", + "status": "existing_write_path_gap_audit_pending", + "sources": [ + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes" + ], + "limits": "Qualify multiple writes, RETURNING and full transaction rollback with current execution phases before enabling the engine hook; no general statement ticket/receipt or savepoint framework is approved." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "native:src/storage/vgi_transaction.cpp", + "native:src/storage/vgi_physical_write.cpp", + "native:src/include/vgi_unary_retry.hpp", + "native:test/run_transactional_write_probe.py", + "tests/catalog/transactional_write_reference.py", + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes", + "manifest:transaction_write_checkpoint_2026_10_22", + "manifest:regression_checkpoint_2026_10_23", + "native:docs/duckdb-2.0-catalog-audit.md:412" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse existing attachment-bound provider transaction, BEGIN/COMMIT/ROLLBACK and streamed init/input/finalize; failed writes invalidate/roll back the whole transaction. No mixed/statement/savepoint/replay profile or coordinator is selected. CTAS and INSERT already have stored-data native1.5 rollback evidence. Expanded native2 ALTER and multiple-DML CTEs remain disabled until complete sequences, RETURNING and failure rollback are qualified. Preserve idempotent completion retries; BEGIN/non-idempotent mutation dispatch is single attempt.", + "remaining_implementation_or_qualification": [ + "No native2 expanded-ALTER/multiple-DML/OLD-NEW/deferred-RETURNING qualification; provider lost-BEGIN orphan cleanup remains provider-owned. Trigger implementation is deferred." + ] + }, + { + "id": "audit-413", + "source_line": 413, + "section": "Attachment and capability boundaries", + "field": "`Supports(RemoteCapability)`", + "audit_evidence": [ + "IS_REMOTE, EXECUTE_QUERY_NODE, EXECUTE_STATEMENT, CONNECT are separate capabilities. VGI has no target implementations; define a SQL-capable worker profile if desired." + ], + "area": "statements", + "disposition": "engine_only", + "target": "separate native RemoteCapability opt-ins", + "status": "reviewed_existing_or_engine_mapping_qualification_pending", + "evidence": [ + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/sql.py:62-72 SqlStatement explicitly grants no execution rights.", + "python:vgi/catalog/query.py:122-150 accepts only explicit/automatic query modes and matching query profiles.", + "python:docs/protocol/vgi-catalog-query-pushdown-design.md:202 explicitly excludes remote DDL/DML/COPY/ATTACH/session control/multi-statements/CONNECT.", + "engine:src/include/duckdb/catalog/catalog.hpp:88-104 and 414-420 defines independent IS_REMOTE/EXECUTE_QUERY_NODE/EXECUTE_STATEMENT/CONNECT capabilities." + ], + "limits": "Existing representation does not enable the deferred lifecycle/execution feature." + }, + { + "status": "statement_reuse_audit_2026_10_25", + "sources": [ + "native:src/vgi_query_function.cpp binding/init and ordinary TABLE registration", + "native:src/vgi_query_rpc.cpp readonly-only offer and validation", + "python:docs/protocol/vgi-catalog-query-pushdown-design.md October25 audit" + ], + "limits": "Native modification registration, complete draining and same-transaction cache invalidation remain open." + } + ], + "decision": "Read-query RPC remains usable; optional statement profile now has a bounded Python reuse implementation. Native automatic query-node and mutation entry points still need qualification; EXECUTE_STATEMENT and raw CONNECT remain false in native VGI." + }, + { + "id": "audit-414", + "source_line": 414, + "section": "Attachment and capability boundaries", + "field": "`RemoteExecute(...)`, `SupportsPushdown(...)`", + "audit_evidence": [ + "New optional execution integration, distinct from ordinary catalog metadata RPCs. Do not claim CONNECT or general SQL delegation merely because table scans work." + ], + "area": "query", + "disposition": "include", + "target": "CatalogQueryPrepareRequest/Result, QueryParameters, existing TABLE execution and release", + "status": "reviewed_explicit_15_automatic_pending", + "evidence": [ + "docs/protocol/shared-catalog-values.md#prepared-read-queries-and-typed-parameters", + "vgi/catalog/query.py", + "vgi/query_parameters.py", + "vgi/query_execution.py", + "vgi/query_provider.py", + "tests/catalog/query_reference.py", + "tests/catalog/test_query_protocol.py", + { + "status": "native_15_explicit_reference_validated", + "source": "native_query_adapter_validation", + "limits": "Automatic native 2.0 optimizer integration remains unqualified." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "python:vgi/catalog/query.py:200,252,281", + "python:vgi/query_parameters.py:40", + "native:src/vgi_query_function.cpp:241,304", + "haybarn:src/include/duckdb/catalog/catalog.hpp:90,417" + ], + "limits": "Source/reference reconciliation only; native/production qualification remains open." + } + ], + "decision": "Keep current status, add evidence that no new remote execution family is required. Automatic eligibility/export/native2 certification remains open; EXECUTE_STATEMENT and CONNECT do not follow from readonly query support." + }, + { + "id": "audit-415", + "source_line": 415, + "section": "Attachment and capability boundaries", + "field": "DDL `transaction_opaque_data`", + "audit_evidence": [ + "Existing requests carry the transaction token. Qualify transaction lifetime across create/alter/drop, CTAS, expanded ALTER, nested writes, and retries. Presence of a token is not proof of rollback." + ], + "area": "transactions", + "disposition": "existing_contract", + "target": "Existing transaction_opaque_data and begin/commit/rollback with verified native1.5 CTAS/INSERT propagation", + "status": "contract_reviewed_existing_lifecycle_native2_qualification_pending", + "evidence": [ + { + "date": "2026-10-15", + "status": "existing_contract_scope_reaffirmed", + "sources": [ + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes" + ], + "limits": "Existing transaction checkpoint evidence remains bounded to tested operations. Audit CTAS, expanded ALTER and nested writes using the existing lifecycle; full transaction rollback is the failure policy, without savepoints or durable mutation recovery." + }, + { + "status": "source_review_2026_10_23", + "sources": [ + "native:src/storage/vgi_transaction.cpp", + "native:src/storage/vgi_physical_write.cpp", + "native:src/include/vgi_unary_retry.hpp", + "native:test/run_transactional_write_probe.py", + "tests/catalog/transactional_write_reference.py", + "docs/protocol/shared-catalog-values.md#transaction-guarantees-and-composite-writes", + "manifest:transaction_write_checkpoint_2026_10_22", + "manifest:regression_checkpoint_2026_10_23", + "native:docs/duckdb-2.0-catalog-audit.md:415" + ], + "limits": "Source/contract reconciliation only. Existing recorded tests retain their original bounded scope; no new tests or native DuckDB 2 qualification." + } + ], + "decision": "Reuse existing attachment-bound provider transaction, BEGIN/COMMIT/ROLLBACK and streamed init/input/finalize; failed writes invalidate/roll back the whole transaction. No mixed/statement/savepoint/replay profile or coordinator is selected. CTAS and INSERT already have stored-data native1.5 rollback evidence. Expanded native2 ALTER and multiple-DML CTEs remain disabled until complete sequences, RETURNING and failure rollback are qualified. Preserve idempotent completion retries; BEGIN/non-idempotent mutation dispatch is single attempt.", + "remaining_implementation_or_qualification": [ + "No native2 expanded-ALTER/multiple-DML/OLD-NEW/deferred-RETURNING qualification; provider lost-BEGIN orphan cleanup remains provider-owned. Trigger implementation is deferred." + ] + } + ], + "proposals": [ + { + "source": "docs/protocol/vgi-protocol-proposed-changes.md", + "areas": [ + "identity", + "logical_types", + "optimizer", + "columns", + "constraints", + "triggers", + "transactions", + "cleanup" + ], + "existing_contracts": [ + "function signatures and typed defaults", + "schema paths", + "Filter v2", + "OLD/NEW changes write mode" + ], + "remaining_reviews": [], + "reconciliation": "October23 review maps existing signature/default/path/Filterv2/OLD-NEW contracts, keeps projection/repeatability in P1, and defers standalone-window lifecycle. Runtime artifact envelope already exists; Bloom/prefix-range evaluators remain unadvertised until qualified. Historical release sequence and blanket filter_encodings removal are superseded." + }, + { + "source": "docs/protocol/vgi-catalog-query-pushdown-design.md", + "areas": [ + "query", + "capabilities", + "transactions", + "statements", + "logical_types" + ], + "reconciliation": "Implemented typed parameter metadata and prepare/release/TABLE execution are included. Automatic native2 adapter/certification remain open; mutating statements and CONNECT are explicit deferrals. No independent query protocol release." + }, + { + "source": "native:docs/multi_input_table_functions.md", + "areas": [ + "multi_input" + ], + "reconciliation": "Reviewed deferral: retain bounded tagged-input coverage and first-class engine feasibility spike. Define new wire only after caller-transaction two-input scheduling/backpressure/completion proof; accept a later coordinated SDK release." + } + ], + "sdk_inventory": { + "sources": [ + "vgi/protocol.py", + "vgi/catalog/catalog_interface.py", + "vgi/codegen/_common.py", + "vgi/catalog/options.py", + "vgi/column_path.py", + "vgi/codegen/catalog_contract_fixtures.py", + "vgi/catalog/capabilities.py", + "vgi/attach_capabilities.py", + "vgi/codegen/cpp_sdk.py" + ], + "required_targets": [ + "vgi-python", + "vgi", + "vgi-go", + "vgi-rust", + "vgi-typescript", + "vgi-java", + "vgi-csharp", + "vgi-c++" + ], + "browser_client": "vgi/http/vgi-client.js", + "gates": [ + "scripts/regen_generated.py --check --require-all", + "make schema_parity", + "make test_languages", + "cross-SDK vectors for every included record and lifecycle" + ], + "limits": "The strict driver now covers native headers, all six worker SDKs including the C++ public headers and registry, the browser client, and shared corpus. Generated representation and unsupported guards do not imply implementation of the new capabilities in every SDK.", + "preparation_revisions": { + "python": "7e61efa", + "go": "983e34b", + "rust": "b6283c3", + "typescript": "4d6ff3e", + "java": "96ca66c", + "csharp": "5ad2026" + }, + "tested_dependencies": { + "python_rpc": "0.50.0", + "python_arrow": "25.0.1", + "go_rpc": "0.32.0", + "go_arrow": "v18.7.0 replaced by Query-farm/arrow-go v18.0.0-20260220022719-2d45cbd918a4", + "rust_rpc": "0.31.4", + "rust_arrow": "60.0.0", + "typescript_rpc": "0.28.0", + "java_rpc_source": "b0af459109f0f26a4e163f219b48b682be897f49", + "csharp_rpc_source": "2062329652ee045466aebe3581dcd60b407db5f2", + "native_arrow": "21.0.0" + } + }, + "gates": { + "P0": { + "status": "complete", + "evidence": [ + { + "status": "inventory_coverage_only", + "sources": [ + "scripts/check_duckdb2_manifest.py", + "tests/test_duckdb2_manifest.py" + ], + "limits": "All 151 audit rows and 19 plan target/candidate areas inventoried, plus identity. Scope/capability/field decisions remain open.", + "superseded_scope_status_by": "combined_protocol_review_2026_10_23" + }, + { + "status": "partial_field_design_reconciled", + "source": "combined_design_review", + "limits": "The historical design review covered specific expression/constraint/transaction/trigger mappings. The 2026-10-15 user decision supersedes overbuilt transaction/recovery proposals and reopens minimum trigger/execution mappings; remaining scope/capability/field reviews are still open.", + "superseded_scope_status_by": "combined_protocol_review_2026_10_23" + }, + { + "status": "scope_review_and_inventory_validation_passed", + "source": "combined_protocol_review_2026_10_23", + "limits": "151 rows / 20 areas / zero unresolved scope mappings; 11 manifest tests passed remotely. P1 contracts/proofs and all later gates remain open." + } + ] + }, + "P1": { + "status": "in_progress", + "evidence": [ + { + "status": "partial_common_contract_proof", + "sources": [ + "docs/protocol/shared-catalog-values.md", + "tests/catalog/test_options.py", + "tests/test_column_path.py", + "conformance/catalog-contracts.json" + ], + "limits": "Shared records and reference semantics only; broader catalog operations and lifecycle contracts remain pending." + }, + { + "status": "partial_operation_lifecycle_proof", + "sources": [ + "tests/catalog/test_schema_options_protocol.py", + "docs/protocol/shared-catalog-values.md" + ], + "limits": "Schema-option reference only. Broader catalog/query operations and complete capabilities remain pending." + }, + { + "status": "historical_design_partial_scope_superseded", + "source": "docs/protocol/shared-catalog-values.md#combined-expression-constraint-transaction-and-trigger-design", + "limits": "No runtime execution evidence was introduced by the historical design checkpoint. Current write/trigger scope follows write_scope_decision_2026_10_15 and reuses existing execution machinery pending a concrete gap audit." + }, + { + "status": "remaining_work_reconciled", + "source": "combined_protocol_review_2026_10_23", + "limits": "Included migrations and optimizer design/proof are explicit blockers; optional families deferred, not qualified." + } + ] + }, + "P2": { + "status": "preparation_only", + "evidence": [ + { + "status": "shared_codec_preparation", + "source": "preparation_validation", + "limits": "The combined protocol is not frozen; remaining records and operations have not been ported." + } + ] + }, + "P3": { + "status": "not_started", + "evidence": [] + }, + "M0": { + "status": "preparation_only", + "evidence": [ + { + "status": "haybarn_refresh_and_source_audit", + "source": "native:docs/duckdb-2.0-plan.md", + "limits": "No pinned native VGI 2.0 build/CI lane or complete compilation inventory yet." + } + ] + }, + "M1": { + "status": "not_started", + "evidence": [] + }, + "M2": { + "status": "not_started", + "evidence": [] + }, + "M3": { + "status": "not_started", + "evidence": [] + }, + "M4": { + "status": "not_started", + "evidence": [] + } + }, + "preparation_validation": { + "engine": "1.5 source 105edd31b57fe7e6d32ee648efad562e45a9f908; existing release unittest", + "coverage": "Shared option/path records and unchanged 2.1 RPC behavior. No new catalog operation or native 2.0 qualification.", + "python_focused_checks": 232, + "manifest_after_scope_guard": 11, + "shared_documentation_examples": 2, + "language_matrix": { + "test_python": { + "passed": 344, + "skipped": 33, + "failed": 0 + }, + "test_go": { + "passed": 336, + "skipped": 40, + "failed": 0 + }, + "test_rust": { + "passed": 323, + "skipped": 42, + "failed": 0 + }, + "test_typescript": { + "passed": 328, + "skipped": 46, + "failed": 0 + }, + "test_java": { + "passed": 324, + "skipped": 37, + "failed": 0 + }, + "test_csharp": { + "passed": 328, + "skipped": 37, + "failed": 0 + } + }, + "language_matrix_exit_code": 0, + "typescript_http": { + "passed": 311, + "skipped": 53, + "failed": 0 + }, + "generated_codec_checks": { + "go": "schema derivation and 6 typed HTTP echo corpus cases", + "rust": "schema parity plus 2 IPC corpus tests", + "typescript": "3 corpus tests with each of 2 Arrow backends; build passed", + "java": "69 schema checks plus 2 corpus tests", + "csharp": "225 schema checks plus 2 corpus tests", + "cpp": "generated schema header syntax-check against Arrow 21.0.0" + }, + "logs": [ + "/tmp/vgi-duckdb2-shared-codegen-tests.log", + "/tmp/vgi-duckdb2-shared-languages.log", + "/tmp/vgi-duckdb2-shared-typescript-launcher.log", + "/tmp/vgi-duckdb2-shared-typescript-http-details.log", + "/tmp/vgi-duckdb2-shared-java.log", + "/tmp/vgi-duckdb2-shared-csharp.log", + "/tmp/vgi-duckdb2-shared-docs.log" + ], + "limitations": [ + "Expected environment/transport skips are recorded separately, not counted as passes.", + "Native tests used the existing 1.5 release binary; only generated C++ schemas changed and received a separate compile check.", + "Shared-record codec proof is not full logical-type semantic support or operation/transaction support." + ] + }, + "schema_options_validation": { + "status": "partial_protocol_implementation_on_15", + "engine": "Haybarn 1.5.5-rc1 / DuckDB v1.5.6-dev149, source 105edd31b57fe7e6d32ee648efad562e45a9f908", + "local_protocol": "Unpublished changing development snapshot; vgi.v2/2.1.0 labels are not a new stable publication.", + "protocol_hash": "98309fc84d6bdb9c01c70986a3ddc53596a9dfaba4325297be22f601e3a4e6a2", + "rpc_methods": 74, + "coverage": [ + "Sealed attachment capability agreement, auth/routing binding, current-provider checks and capability-specific metadata etags", + "Schema option definitions/create/SET/RESET/readback through real Python HTTP and MetaWorker routes", + "Persistent SQLite reference: reconnect, transaction visibility, rollback, idempotent commit and conflict rejection", + "Coordinated generated fields/common records, cross-SDK IPC corpus and rejection before unsupported mutations" + ], + "limits": [ + "Remaining catalog/query/type/optimizer contracts and per-object execution requirements are not implemented", + "Native 1.5 adapter sends no new catalog profile and does not expose schema-option SQL", + "The SQLite schema reference is not a mixed DDL/DML or distributed atomicity proof", + "No complete P0-P3 or M0-M4 exit gate; no native DuckDB 2.0 build qualification or public release" + ], + "revisions": { + "native": "ae56db3fb4a1a0a89073e30d2ea80bdbb8fe4b6a", + "go": "518de17db624f16d0aa19224b2fe57ed7180559c", + "rust": "ef4ce824422ab0d5efa6d26a1cad00586f461ad6", + "typescript": "4aa4551c135238a51c345f0825820efcbc94e30e", + "java": "9563827e35c320bea30185d21097b58e8158e3ea", + "csharp": "4a21516f27f7830e94bf3edaf0f9a75e30655172", + "cpp": "191ae71945ec9339a358190dceff8b065a42c5eb", + "java_rpc": "a1676f5c16c6bf27da8e9f579dbb5414de9d15a2" + }, + "python": { + "focused_passed": 675, + "ruff": "passed, including format check", + "mypy_sources": 209, + "strict_generation": "--check --require-all passed", + "full_suite_limit": "Earlier full run: 3437 passed, 119 skipped, 5 failures. Four suite/generator/doc issues corrected and rechecked; the fifth reads a pre-existing global /tmp/vgi-http-test-server.log with obsolete request_data records. That unrelated artifact was not erased; no claim of a fully green Python suite." + }, + "final_sdk_lanes": { + "python": { + "passed": 344, + "skipped": 33, + "failed": 0 + }, + "go": { + "passed": 336, + "skipped": 40, + "failed": 0 + }, + "rust": { + "passed": 323, + "skipped": 42, + "failed": 0 + }, + "typescript": { + "passed": 328, + "skipped": 46, + "failed": 0 + }, + "java": { + "passed": 324, + "skipped": 37, + "failed": 0 + }, + "csharp": { + "passed": 328, + "skipped": 37, + "failed": 0 + } + }, + "matrix_run_note": "Initial make test_languages failed on the outdated development-hash oracle, a native cancellation shutdown abort, and a Java21 carrier starvation stall. All affected lanes were rerun successfully after fixes; C# completed successfully with the fixed native binary. Counts include two separate process-crash probes in Python and Java. Skips do not count as passes.", + "typescript_http": { + "passed": 311, + "skipped": 53, + "failed": 0 + }, + "cpp_sdk": { + "passed": 68, + "failed": 0, + "coverage": "All CTest targets including schema corpus, routing, stdio and HTTP" + }, + "native_dispatcher": { + "cases": 9, + "assertions": 37, + "coverage": "Explicit/first-use start, concurrent queues, last-connection database release, reopen and active cancellation interruption" + }, + "java_rpc_21": { + "tests": 608, + "skipped": 3, + "failures": 0, + "regression": "Carrier-starvation regression fails before the stream adapter fix and passes afterwards on Java21" + }, + "logs": [ + "/tmp/vgi-duckdb2-schema-options-final-python.log", + "/tmp/vgi-duckdb2-schema-options-languages.log", + "/tmp/vgi-duckdb2-cancel-fixed-sdk-lanes.log", + "/tmp/vgi-typescript-test.log", + "/tmp/vgi-duckdb2-typescript-http-results.log", + "/tmp/vgi-duckdb2-java-fixed-integration.log", + "/tmp/vgi-duckdb2-cpp-sdk-tests.log", + "/tmp/vgi-duckdb2-cancel-fix-tests.log", + "/tmp/vgi-duckdb2-java-rpc-all21.log" + ] + }, + "table_layout_validation": { + "status": "partial_protocol_implementation_on_15", + "rpc_methods": 77, + "protocol_hash": "880e67e755301062698475d23ae8922a6b868d8d0b70ddb195edd96f6be385a4", + "engine": "Pinned DuckDB 1.5 reference; native 2.0 not qualified", + "log_prefix": "/tmp/vgi-duckdb2-table-layout-", + "limits": [ + "All P0-P3 and M0-M4 exit gates remain open; 137 scope/capability/field reviews remain.", + "Native Python integration passed; native Go/Rust/TypeScript/Java/C#/HTTP/browser integration lanes have not been rerun for this increment.", + "Expression-profile semantic/type qualification and production providers remain incomplete; native 1.5 offers no new profiles.", + "Go nullable-map codec requires the isolated RPC-Go fix before publication.", + "The full Python suite has not been requalified; the prior schema-options validation records its limits." + ], + "python": { + "focused_passed": 703, + "layout_reference_cases": 17, + "additional_note": "15 of the 17 layout cases were included in the focused run; the final 17 plus 11 manifest checks passed together. Counts must not be added.", + "generation_documentation": "289 passed, 30 skipped; one docstring failure corrected, then both docstring checks passed.", + "ruff": "lint and format passed", + "mypy_sources": 210, + "strict_generation": "--check --require-all passed" + }, + "sdk_units": { + "go": "All ./vgi/... packages passed with the isolated RPC-Go dependency", + "rust": { + "passed": 481, + "ignored": 4 + }, + "typescript": { + "passed": 596, + "skipped": 68, + "flechette_codec_passed": 5, + "bundle_build": "passed" + }, + "java": { + "passed": 612, + "skipped": 2 + }, + "csharp": { + "passed": 592 + }, + "cpp_ctest": { + "passed": 68 + } + }, + "native_python": { + "release_build": "passed", + "passed": 344, + "skipped": 33, + "crash_probes_included": 2 + }, + "dependencies": { + "go_rpc": { + "revision": "1fa10b93d9493a81949ac6ffce8e7359bd260b9a", + "regression": "Nullable populated/empty/absent maps panic before fix, pass afterward; focused race checks pass.", + "qualification_limit": "Broader RPC-Go suite fails TestNetworkCannotAttachAdvertisedLocalSegment at ShmCreate with macOS file name too long, outside the changed decoder path.", + "test_environment": { + "GOFLAGS": "-mod=readonly", + "GOWORK": "/tmp/vgi-duckdb2-table-layout-go.work" + } + }, + "java_rpc": { + "revision": "a1676f5c16c6bf27da8e9f579dbb5414de9d15a2", + "environment": "VGI_RPC_JAVA_DIR=/Users/rusty/Development/vgi-rpc-java-duckdb2" + } + }, + "revisions": { + "native": "89a00179bb99edfc2803c66884429764cca6e8b2", + "go": "ec0150f383462e4f07473fa7fe9789f37dee8823", + "rust": "0561b9d62f4a0a8172b189a4abd9270af1bb1194", + "typescript": "d88297c5e1ae659823b89c4d472156d848a38a67", + "java": "4e90c0845edad5bef027c9b7f0a7812aaf726634", + "csharp": "34bda80895612dada0d2d451f5b1bb77a0d18211", + "cpp": "f7f593c71fe3b26b0b360655583bdbcf92110304" + } + }, + "column_paths_validation": { + "status": "partial_protocol_implementation_on_15", + "rpc_methods": 77, + "protocol_hash": "4b77dc42e39231dec67e31739ab31ae1f7291a8f92ea4e6767db61f39afb5dfd", + "version_requirement": "Major transition required if the current required raw-path replacements are retained; development version strings remain unpublished placeholders.", + "engine": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "environment": "Linux ARM64; local source edits synchronized to the task-owned EC2 workspace", + "remote_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "source_manifest": "state/sources.json", + "python": { + "passed": 977, + "skipped": 30, + "final_column_and_manifest_passed": 21, + "counts_overlap": true, + "mypy_sources": 210, + "ruff": "passed", + "strict_generation": "--check --require-all passed", + "scope": "Catalog, shared codecs, generation, inventory, documentation and transactor startup; not the full Python suite." + }, + "sdk_units": { + "go": "13 tested packages passed (go test ./...)", + "rust": { + "passed": 482, + "ignored": 4 + }, + "typescript": { + "passed": 599, + "skipped": 68, + "flechette_codec_passed": 8, + "bundle_build": "passed" + }, + "java": { + "passed": 654, + "skipped": 2 + }, + "csharp": { + "passed": 593 + }, + "cpp_ctest": { + "passed": 68, + "failed": 0 + } + }, + "native": { + "release_build": "passed", + "column_sql_assertions": 82, + "language_matrix": { + "python": { + "passed": 344, + "skipped": 33, + "failed": 0 + }, + "go": { + "passed": 336, + "skipped": 40, + "failed": 0 + }, + "rust": { + "passed": 323, + "skipped": 42, + "failed": 0 + }, + "typescript": { + "passed": 328, + "skipped": 46, + "failed": 0 + }, + "java": { + "passed": 324, + "skipped": 37, + "failed": 0 + }, + "csharp": { + "passed": 328, + "skipped": 37, + "failed": 0 + } + }, + "language_matrix_exit_code": 0, + "python_java_crash_probes": "Two separate crash probes per language included in their totals.", + "cpp": { + "passed": 324, + "skipped": 47, + "assertions": 12042, + "http_bearer_passed": 2, + "http_bearer_assertions": 13, + "exit_code": 0, + "limits": "Separate C++ harness includes six .test_slow cases; its inventory and skips differ from the six-language matrix." + } + }, + "fixes": [ + "Preserve ADD COLUMN defaults through the shared native column encoder.", + "Invalidate metadata loaded during an aborted remote transaction even when the committed catalog version is unchanged.", + "Mark a catalog listing incomplete after removing an altered entry so metadata scans reload it.", + "Normalize TypeScript RPC Arrow list scalars without splitting literal dotted identifiers.", + "Update the C# bad-enum fixture delegation signatures alongside generated service methods." + ], + "dependencies": { + "go_rpc": "1fa10b93d9493a81949ac6ffce8e7359bd260b9a", + "java_rpc": "a1676f5c16c6bf27da8e9f579dbb5414de9d15a2", + "cpp_libsodium": "Pinned 1.0.21 plus task-local overlay applying upstream GCC/aarch64 fix 6702f69bef6044163acc7715e6ac7e430890ce78; no repository dependency pin changes.", + "go_rpc_validation": "Full go test ./... passed on Linux ARM64, including the shared-memory case that failed on macOS." + }, + "limits": [ + "This increment closes no P0-P3 or M0-M4 gate.", + "Shared expressions, generated columns, complete column attributes/dependencies, required-filter operation migration and native 2.0 bindings remain open.", + "The SQL bridge is a durable reference with count INSERT only; it is not the older transactor or proof of all production write semantics.", + "The older Python transactor requires fork-only cursor APIs absent from the pinned Python engine; its startup annotation fix does not resolve that limitation.", + "Expected skips do not count as passes; SDK schema/codec support does not advertise new profiles." + ], + "logs": [ + "logs/python-qualified.log", + "logs/python-column-final.log", + "logs/python-reference-lint.log", + "logs/python-mypy.log", + "logs/generation-check.log", + "logs/native-build-final.log", + "logs/native-column-paths-final.log", + "logs/native-languages-qualified.log", + "logs/native-typescript-final.log", + "logs/go-test.log", + "logs/go-rpc-test.log", + "logs/rust-test.log", + "logs/ts-test-final.log", + "logs/ts-flechette-final.log", + "logs/ts-build-final.log", + "logs/java-test.log", + "logs/csharp-test.log", + "logs/cpp-configure-final.log", + "logs/cpp-build.log", + "logs/cpp-test.log", + "logs/native-cpp.log", + "state/cpp-native/run.log", + "state/cpp-native/worker.log" + ], + "revisions": { + "go": "029e087f7ff76fc69188dffb26bc99854e084550", + "rust": "554c9763899f8b671635c919ca6eb5ba5c4479da", + "typescript": "fabd48774cff80641891649be545eb11af6c4f53", + "java": "5c6348bd28126e7ddba29d79047e5754d99de61e", + "csharp": "927ac7cd132faf9229478711c45ba6db239ee8ef", + "cpp": "dde72f5349ca324e0c62738c3ece8882d530f5ab" + } + }, + "required_filter_paths_validation": { + "status": "partial_protocol_implementation_on_15", + "engine": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "remote_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "source_manifest": "state/sources.json", + "rpc_methods": 77, + "protocol_hash": "4b77dc42e39231dec67e31739ab31ae1f7291a8f92ea4e6767db61f39afb5dfd", + "hash_limit": "TableInfo is nested IPC inside binary RPC results; payload-only schema changes do not change the RPC reflection hash. All-SDK schema/corpus qualification and a coordinated major transition remain required.", + "python": { + "passed": 912, + "scope": "Catalog, raw paths, shared corpus, generated artifacts, generation inventory and manifest; not the full Python suite.", + "mypy_sources": 210, + "ruff": "passed", + "strict_generation": "--check --require-all passed", + "final_manifest_corpus_regeneration_passed": 21, + "counts_overlap": true + }, + "native": { + "release_build": "passed", + "column_and_required_path_sql_assertions": 153, + "required_path_sql_assertions": 71, + "malformed_metadata_modes": 13, + "language_matrix": { + "python": { + "passed": 344, + "skipped": 33, + "failed": 0 + }, + "go": { + "passed": 336, + "skipped": 40, + "failed": 0 + }, + "rust": { + "passed": 323, + "skipped": 42, + "failed": 0 + }, + "typescript": { + "passed": 328, + "skipped": 46, + "failed": 0 + }, + "java": { + "passed": 324, + "skipped": 37, + "failed": 0 + }, + "csharp": { + "passed": 328, + "skipped": 37, + "failed": 0 + } + }, + "python_java_crash_probes": "Two separate crash probes per language included in their totals.", + "qualification_runs": { + "initial_make_test_languages_exit_code": 2, + "initial_failures": "Python/Go/Rust shared SQL expectation assumed bare wrapper; the diagnostic now quotes that identifier. TypeScript, Java and C# lanes passed after the expectation was updated.", + "final_make_test_python_test_go_test_rust_exit_code": 0, + "logs": [ + "logs/required-native-languages.log", + "logs/required-native-rerun.log", + "logs/required-native-typescript.log", + "logs/required-native-paths-qualified.log" + ] + }, + "cpp": { + "passed": 324, + "skipped": 47, + "assertions": 12042, + "http_bearer_passed": 2, + "http_bearer_assertions": 13, + "exit_code": 0, + "log": "logs/required-native-cpp-final.log", + "limits": "Includes six .test_slow cases, unlike the six-language matrix." + } + }, + "limits": [ + "No protocol version published; required raw paths remain part of the combined major transition.", + "No P0-P3 or M0-M4 gate closed.", + "The DuckDB 2.0 optimizer/API port and the other catalog/query/type contracts remain open." + ], + "sdk_units": { + "go": "13 tested packages passed", + "rust": { + "passed": 482, + "ignored": 4 + }, + "typescript": { + "passed": 599, + "skipped": 68, + "flechette_codec_passed": 8, + "bundle_build": "passed" + }, + "java": { + "passed": 655, + "skipped": 2 + }, + "csharp": { + "passed": 593, + "solution_build": "passed" + }, + "cpp_ctest": { + "passed": 68, + "failed": 0 + } + } + }, + "view_aliases_validation": { + "status": "partial_protocol_implementation_on_15", + "engine": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "remote_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "source_manifest": "state/sources.json", + "rpc_methods": 77, + "protocol_hash": "9e1ed05cdcc6e812f9a6cfda36ecaa699005e30680e511489109180251ee6872", + "contract": { + "ViewInfo.aliases": "Required ordered list of raw output identifiers; [] retains SELECT-derived output names. Partial aliases rename the leading outputs.", + "catalog_view_create.aliases": "Optional list; NULL/[] both mean no explicit aliases. Definition remains SELECT text.", + "native_validation": "Reject absent/NULL aliases lists and NULL/empty/NUL identifiers before materializing a view." + }, + "native": { + "release_build": "passed", + "reference_assertions": 172, + "reference_cases": 3, + "malformed_view_modes": 5, + "language_matrix": { + "python": { + "passed": 345, + "skipped": 33, + "failed": 0 + }, + "go": { + "passed": 337, + "skipped": 40, + "failed": 0 + }, + "rust": { + "passed": 324, + "skipped": 42, + "failed": 0 + }, + "typescript": { + "passed": 329, + "skipped": 46, + "failed": 0 + }, + "java": { + "passed": 325, + "skipped": 37, + "failed": 0 + }, + "csharp": { + "passed": 329, + "skipped": 37, + "failed": 0 + } + }, + "python_java_crash_probes": "Two separate crash probes per language included in their totals.", + "cpp": { + "passed": 325, + "skipped": 47, + "assertions": 12065, + "http_bearer_passed": 2, + "http_bearer_assertions": 13, + "exit_code": 0, + "log": "logs/views-native-cpp.log" + }, + "qualification_runs": { + "initial_make_test_languages_exit_code": 2, + "initial_failure": "TypeScript view handler passed opaque Arrow dictionary data instead of decoding on_conflict; the new IF NOT EXISTS assertion caught this.", + "final_full_typescript_lane_exit_code": 0, + "logs": [ + "logs/views-native-languages.log", + "logs/views-native-typescript-qualified.log", + "logs/views-native-reference.log" + ] + }, + "additional_transport_probe": { + "typescript_flechette_stdio": "unqualified: ATTACH fails in the RPC request validator comparing the backend Binary types, before alias operations.", + "log": "logs/views-native-ts-flechette.log", + "codec_scope": "The seven Flechette shared-corpus codec tests pass; this does not qualify its stdio transport." + } + }, + "limits": [ + "Secure views, metadata creation, view-column comments, name resolution, dependencies, and durable view transactions remain pending.", + "The older Python transactor fixture rejects explicit aliases because its view reader cannot preserve them.", + "No P0-P3 or M0-M4 gate is complete." + ], + "python": { + "passed": 732, + "scope": "Catalog tests, shared corpus, generated registry/client and request inventory; not the full Python suite.", + "mypy_sources": 210, + "ruff": "check and format passed for 14 changed files", + "strict_generation": "--check --require-all passed", + "final_generation_manifest_corpus_checks": 27, + "counts_overlap": true + }, + "sdk_units": { + "go": "13 tested packages passed", + "rust": { + "passed": 484, + "ignored": 4 + }, + "typescript": { + "passed": 600, + "skipped": 68, + "flechette_codec_passed": 7, + "bundle_build": "all four entries passed" + }, + "java": { + "passed": 656, + "skipped": 2 + }, + "csharp": { + "passed": 594, + "solution_build": "passed" + }, + "cpp_ctest": { + "passed": 68, + "failed": 0 + } + }, + "validation_repairs": [ + "Native request generator now chooses the existing optional-string-list builder for nullable list fields.", + "Rust event replay defaults missing aliases in older persisted events to the previous query-derived names; new aliases survive replay.", + "Rust continuation-size regression accounts for one 64-byte IPC padding boundary while retaining the bounded-growth assertion.", + "Remote sync uses checksums and fresh changed-file timestamps. C++ objects were rebuilt after finding stale header consumers." + ], + "sdk_revisions": { + "go": "01f99bc5e5ff1e8ddf934733acfd8750fb53f56b", + "rust": "34245b2921d84a2fbd4fb3db9bae4f49b35500f5", + "typescript": "e7348d193dc001da1294d3c40e490e62a90d5ac9", + "java": "c7c4be091e26c79ab083af88097d69d690404686", + "csharp": "a6e960b2ff2adc314f775c15d1dc6c7462884dc1", + "cpp": "1823d765343dc8a4f456652bf379da3682e55e5c" + }, + "native_revision": "84536eda52f648f719db6f1ffc218f332a126aa4" + }, + "view_security_validation": { + "status": "partial_protocol_implementation_on_15", + "protocol_name": "vgi.v2", + "protocol_version": "2.1.0", + "method_count": 77, + "protocol_hash": "b1db9b01e3c78a746e627fe18a4e0e0c191389dce42f15368427067f071cdecc", + "publication": "Unpublished combined successor snapshot; no intermediate release.", + "contract": "docs/protocol/view-security.md", + "native_engine": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "profiles": [ + "vgi.view.barrier.read.v1", + "vgi.view.barrier.create.v1" + ], + "implemented": [ + "Required explicit regular/barrier mode in ViewInfo and CREATE wire parameter across seven SDKs.", + "Python selected/current capability enforcement for get, listing, snapshots and ETag revalidation.", + "Python mutation preflights, create/read dependency and transactional DDL guarantees.", + "Durable SQLite metadata reference: public HTTP client, reconnect, rollback/commit, replacement, rename and comment preservation.", + "Native1.5 rejects barrier, absent, NULL, wrongly typed and unknown security modes through independently asserted get/list/bulk routes.", + "Shared regular/barrier codec vectors; bundled ordinary providers reject unsupported CREATE modes before effects, including IGNORE/REPLACE." + ], + "python": { + "passed": 758, + "scope": "Catalog tests, shared corpus, generated registry/client and request inventory; not the full Python suite.", + "mypy_sources": 210, + "ruff": "passed for the 15 changed Python files", + "strict_generation": "--check --require-all passed", + "final_contract_manifest_generation_checks": 74, + "counts_overlap": true + }, + "native": { + "build": "passed", + "reference_cases": 3, + "reference_assertions": 218, + "language_matrix": { + "python": { + "passed": 345, + "skipped": 33, + "remaining_failures": 0 + }, + "go": { + "passed": 337, + "skipped": 40, + "remaining_failures": 0 + }, + "rust": { + "passed": 324, + "skipped": 42, + "remaining_failures": 0 + }, + "typescript": { + "passed": 329, + "skipped": 46, + "remaining_failures": 0 + }, + "java": { + "passed": 325, + "skipped": 37, + "remaining_failures": 0 + }, + "csharp": { + "passed": 329, + "skipped": 37, + "remaining_failures": 0 + } + }, + "logs": [ + "logs/security-native.log", + "logs/security-final-reference.log", + "logs/security-native-languages.log", + "logs/security-native-ts.log", + "logs/security-native-cpp.log", + "logs/security-python-reflection-crash.log", + "logs/security-cpp-reflection.log" + ], + "python_java_crash_probes": "Two separate crash cases per language included in their totals.", + "cpp": { + "qualified_cases": 325, + "assertions": 12065, + "skipped": 47, + "http_bearer_passed": 2, + "http_bearer_assertions": 13, + "initial_full_run_passed": 324, + "initial_full_run_failed": 1, + "initial_exit_code": 1, + "reflection_recheck_passed": 1, + "reflection_recheck_exit_code": 0 + }, + "qualification_runs": { + "initial_make_test_languages_exit_code": 2, + "initial_failure": "Python and C++ had loaded the old expected SQL reflection hash. Other native language lanes used the corrected expectation and passed.", + "repair": "Updated the shared SQL expected hash to the computed successor hash; reran the reflection test separately on Python and C++, then ran the two Python crash probes skipped after its initial lane failure.", + "remaining_failures": 0, + "logs": [ + "logs/security-native-languages.log", + "logs/security-native-ts.log", + "logs/security-native-cpp.log", + "logs/security-native-cpp-full.log", + "logs/security-python-reflection-crash.log", + "logs/security-cpp-reflection.log" + ] + } + }, + "open": [ + "All P0-P3 and M0-M4 gates remain open; 137 scope/capability/field reviews remain.", + "Native2 build, security_type mapping and optimizer barrier execution, refresh/rebind and dependent-query qualification.", + "Barrier capability selection/enforcement and capable mutable providers in the six non-Python SDKs.", + "Atomic concurrent replacement, authorization and dependency semantics; worker preflights are not a concurrency primitive.", + "Creation comments/tags/column comments, view-column comment mutation and owner-relative query resolution.", + "Previously recorded Flechette stdio ATTACH and old Python transactor qualification gaps remain open." + ], + "sdk_units": { + "go": { + "tested_packages": 13, + "failed": 0 + }, + "rust": { + "passed": 486, + "ignored": 4, + "failed": 0 + }, + "typescript": { + "passed": 602, + "skipped": 68, + "flechette_codec_passed": 8, + "bundle_build": "all four entries passed" + }, + "java": { + "passed": 658, + "skipped": 2, + "failed": 0 + }, + "csharp": { + "passed": 596, + "failed": 0, + "solution_build": "passed" + }, + "cpp_ctest": { + "passed": 68, + "failed": 0 + } + }, + "sdk_revisions": { + "go": "ae7bd2acfbc3ab2aa0050636bb3ed23432bd388b", + "rust": "f1015f9acbd0f52756fc3b76efd4eeb24508161d", + "typescript": "28f981fc96cdf27ff3723c0df9aa92e3460245fb", + "java": "3f866bd303b89ed5f2a6fd3787b1a969f0ff3cac", + "csharp": "f7a47f5c3bd29975400c6846c675d61f8c81e10d", + "cpp": "47a6fc4f0a28d640defb0a2b037d3754b81560fa" + }, + "validation_repairs": [ + "C# registry generator now renders literal string defaults, needed for security=regular.", + "Native discovery is lazy: bulk metadata is checked when first decoded, not necessarily during ATTACH. Tests force get/list/bulk paths and fail on unexpected fallback.", + "Updated handwritten SDK test requests/initializers and expected reflection hashes for the new wire field.", + "Final Rust/C++ rebuilds passed after limiting formatter changes to the edited code." + ], + "native_revision": "31036603c51e085cec3fd000bde2d776d4882c3d" + }, + "view_metadata_validation": { + "status": "qualified_increment_on_15_remaining_scope_open", + "engine_pin": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "native_engine": "Haybarn/DuckDB 1.5; no native2 build in this increment", + "protocol_methods": 78, + "reflection_hash": "18fc1d304bafc3ea667bc77e20dbbd0028444ec7587cca7f03a428067942a4f7", + "publication": "unpublished coordinated successor; no release gate closed", + "remote_root": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "edit_build_policy": "Edit isolated worktrees locally, sync byte inventories, build and test on EC2 only.", + "contract": { + "creation": [ + "nullable comment distinguishes NULL and empty", + "nullable tags and column_comments default to empty maps", + "REPLACE resets omitted metadata; IGNORE preserves existing object", + "empty/NUL output keys rejected before effects" + ], + "mutation": [ + "catalog_view_column_comment_set uses exactly one raw component", + "NULL removes a column comment; empty string stores a comment", + "missing views obey ignore_not_found; malformed paths still reject", + "preserve aliases/security/tags/other comments" + ], + "native": [ + "route view-column COMMENT separately from table-column COMMENT", + "bind and validate output existence before mutation", + "preserve metadata in CreateViewInfo and copy-schema path", + "normalize explicit aliases at initial binding", + "invalidate view cache" + ] + }, + "python": { + "catalog_and_contract_checks_passed": 784, + "skipped": 1, + "skip_reason": "cross-SDK worker argv mapping is supplied only in the separate conformance run", + "mypy_sources": 210 + }, + "public_rpc": { + "sdk_cases": 7, + "pytest_passed_including_environment_check": 8, + "coverage": [ + "creation", + "get/list", + "tags", + "NULL/empty comments", + "IGNORE/REPLACE", + "invalid keys and paths before mutation", + "missing views", + "drop" + ], + "durable_reference": "sealed HTTP, reconnect, commit/rollback and regular/barrier metadata", + "rejected_request_versions": "Catalog versions remain unchanged for invalid creation keys and column paths, including IGNORE/REPLACE." + }, + "native_targeted": { + "sdks": [ + "python", + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "cases_per_sdk": 2, + "assertions_per_sdk": 56, + "reference_cases": 3, + "reference_assertions": 218 + }, + "repairs": [ + "Python MetaWorker routing includes the new RPC", + "TypeScript decodes opaque map parameters and materializes raw path lists", + "Go wire ViewInfo.Comment distinguishes nil and empty", + "native runtime request/response registry covers all 78 methods, including five earlier option/layout methods" + ], + "remaining": [ + "full SDK capability negotiation", + "owner-relative query resolution", + "dependencies and fixed output contracts", + "production concurrent view lifecycle guarantees", + "native DuckDB2 barrier execution", + "Flechette stdio transport remains unqualified" + ], + "sdk_revisions": { + "go": "81db68eaa07a30035783767fbafb1b97a038cf9f", + "rust": "799dc68d47e8b532d3123ff413b15459a8d462c1", + "typescript": "f69e8b9f7b393432dd8105fe4b1d48365d7ae3f2", + "java": "81e84af967189a68988aff71109aaff0ef9ebf3a", + "csharp": "b01d9a48536001e27eed9727cd2392bf73d3ea93", + "cpp": "59fdfb57cb3c5bc98370b0f7ce04d53b36c2bf63" + }, + "sdk_units": { + "go": { + "tested_packages": 13, + "failed": 0 + }, + "rust": { + "passed": 486, + "ignored": 4, + "failed": 0 + }, + "typescript": { + "passed": 602, + "skipped": 68, + "failed": 0, + "bundle_build": "all four entries passed", + "flechette_codec_passed": 8 + }, + "java": { + "passed": 658, + "skipped": 2, + "failed": 0 + }, + "csharp": { + "passed": 598, + "failed": 0, + "solution_build": "passed" + }, + "cpp_ctest": { + "passed": 68, + "failed": 0 + } + }, + "native_full_cpp": { + "qualified_cases": 326, + "assertions": 12098, + "skipped": 47, + "http_bearer_cases": 2, + "http_bearer_assertions": 13, + "exit_code": 0 + }, + "quality": [ + "strict all-SDK generation check passed", + "Ruff passed on modified Python sources and tests", + "mypy passed on 210 sources" + ], + "logs": [ + "logs/metadata-qualified-python.log", + "logs/metadata-cross-sdk-versions.log", + "logs/metadata-qualified-native.log", + "logs/metadata-native-reference.log", + "logs/metadata-native-languages.log", + "logs/metadata-native-ts.log", + "logs/metadata-native-cpp.log", + "state/metadata-cpp-native/run.log", + "logs/metadata-validated-go.log", + "logs/metadata-validated-rust.log", + "logs/metadata-qualified-ts.log", + "logs/metadata-validated-java.log", + "logs/metadata-validated-csharp.log", + "logs/metadata-validated-cpp.log", + "logs/metadata-flechette-codec-qualified.log" + ], + "native_revision": "5196af88ddb3bdd877f1f89b8b6e31e7327bbea8", + "native_language_matrix": { + "python": { + "passed": 346, + "skipped": 33, + "failed": 0 + }, + "go": { + "passed": 338, + "skipped": 40, + "failed": 0 + }, + "rust": { + "passed": 325, + "skipped": 42, + "failed": 0 + }, + "typescript": { + "passed": 330, + "skipped": 46, + "failed": 0 + }, + "java": { + "passed": 326, + "skipped": 37, + "failed": 0 + }, + "csharp": { + "passed": 330, + "skipped": 37, + "failed": 0 + } + }, + "native_language_matrix_exit_code": 0, + "python_java_crash_probes": "Two separate crash cases per language included in their totals.", + "native_build": "passed on unchanged 1.5 pin", + "source_inventory": "state/sources.json and state/source-verification.json in the unique remote workspace record exact committed heads and source bytes.", + "final_contract_manifest_checks": 23, + "source_verification": { + "repositories": 15, + "files": 33014, + "errors": 0 + }, + "counts_overlap": true + }, + "schema_metadata_validation": { + "status": "creation_metadata_qualified_on_duckdb_15", + "engine_revision": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "protocol_hash": "18fc1d304bafc3ea667bc77e20dbbd0028444ec7587cca7f03a428067942a4f7", + "protocol_method_count": 78, + "wire_changes": [], + "contract": [ + "Reuse catalog_schema_create comment/tags/on_conflict and SchemaInfo metadata.", + "NULL and empty comments are distinct; omitted/NULL tags mean empty.", + "ERROR/IGNORE preserve metadata and catalog version. Empty-schema REPLACE resets omitted metadata.", + "Get/list/catalog_contents preserve metadata; estimated object counts may differ by route.", + "Raw schema path components distinguish literal dotted names from nested names." + ], + "ec2_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "checks": { + "python_catalog_and_codegen": { + "passed": 769, + "skipped": 2 + }, + "public_rpc_schema_and_view_lifecycles": { + "passed": 15, + "sdk_count": 7 + }, + "manifest_corpus_native_inventory": { + "passed": 23 + }, + "go": { + "packages": 13 + }, + "typescript": { + "passed": 602, + "skipped": 68, + "bundle_entries": 4 + }, + "cpp": { + "ctests_passed": 68 + }, + "ruff": "passed", + "mypy": { + "sources": 210 + }, + "all_sdk_generation": "passed", + "native_build": "passed", + "native_parsed_create_copy_cache": { + "sdk_count": 7, + "test_cases_per_sdk": 1, + "assertions_per_sdk": 99 + }, + "native_reference": { + "passed": 3, + "assertions": 218 + }, + "source_verification": { + "repositories": 15, + "files": 33017, + "errors": 0 + } + }, + "limits": [ + "Native DuckDB 2.0 remains unimplemented.", + "Schema comment/tag mutation and full capability mapping remain pending.", + "Nonempty/descendant schema REPLACE/drop dependency semantics remain pending.", + "Transaction qualification is the durable Python reference, not every SDK/provider.", + "Go static schema descriptors retain historical empty-string default; its older writable provider rejects tags and empty comments.", + "Older Python transactor rejects schema creation metadata and REPLACE.", + "Previously recorded Flechette stdio limitation remains open.", + "No combined protocol freeze, phase completion or release." + ], + "logs": [ + "schema-native-build.log", + "schema-native-catalog-build.log", + "schema-native-{python,go,rust,typescript,java,csharp,cpp}.log", + "schema-cross-sdk.log", + "schema-python.log", + "schema-go.log", + "schema-ts.log", + "schema-cpp.log", + "schema-native-reference.log" + ], + "native_revision": "11a5fc5ee691712086d9fe25f6a7f739d0735a25", + "python_base_revision": "f490399eccdafec1a82924cb4622784b7b03e542", + "sdk_revisions": { + "go": "195324414236dac994512d603702bc9d317a31cd", + "rust": "799dc68d47e8b532d3123ff413b15459a8d462c1", + "typescript": "b7eff8767719d4b9fe85d188b51c08879dc9f112", + "java": "81e84af967189a68988aff71109aaff0ef9ebf3a", + "csharp": "b01d9a48536001e27eed9727cd2392bf73d3ea93", + "cpp": "47d1aa24e0cb9fff0da9539c619158ac71f3fb32" + } + }, + "schema_metadata_mutation_validation": { + "status": "reference_and_shared_codecs_qualified_on_duckdb_15", + "engine_revision": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "haybarn_2_source_revision": "e1784fa4231072d7152b8aaa20977bdb8ff4ace3", + "protocol_hash": "f98de1aecf595fb5866082998a4a6d77d1850e781edce50b5b08ffc163e83b94", + "protocol_method_count": 79, + "method": "catalog_schema_alter_metadata", + "record": "CatalogMetadataChanges", + "ddl_profile": "vgi.schema.metadata.alter.v1", + "transaction_profile": "vgi.transaction.ddl.v1", + "contract": [ + "Four complete fields distinguish unchanged, NULL and empty comments and literal tag SET/REMOVE.", + "Duplicate SET keys/removals, overlapping SET/REMOVE, incomplete/wrong wire shape, extra or absent batches, trailing bytes and malformed schema paths reject before provider effects.", + "Empty/effective no-op patches preserve versions and etags; missing targets obey explicit IGNORE.", + "Changes preserve options and unrelated metadata, advance transaction-visible versions, and invalidate committed snapshots on commit.", + "Selected and currently available DDL support is required; transaction handles also require DDL transaction guarantees.", + "A routed/reconnected attachment preserves its sealed agreement." + ], + "ec2_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "checks": { + "native_build": "passed", + "native_schema_create_copy_cache": { + "sdk_count": 7, + "test_cases_per_sdk": 1, + "assertions_per_sdk": 99 + }, + "native_protocol_reflection": { + "sdk_count": 7, + "test_cases_per_sdk": 1, + "assertions_per_sdk": 2 + }, + "native_reference": { + "test_cases": 3, + "assertions": 218 + }, + "python_catalog_and_codegen": { + "passed": 803, + "skipped": 3, + "skip_reason": "External-worker cases require VGI_METADATA_WORKERS; separately configured run passes all 22 checks." + }, + "manifest_corpus_native_inventory": { + "passed": 23 + }, + "go": { + "packages": 13 + }, + "rust": { + "passed": 487, + "ignored": 4 + }, + "typescript": { + "passed": 604, + "skipped": 68, + "bundle_entries": 4 + }, + "java": { + "passed": 660, + "skipped": 2 + }, + "csharp": { + "passed": 602 + }, + "cpp": { + "ctests_passed": 68 + }, + "flechette_codec": { + "passed": 9 + }, + "ruff": "passed", + "mypy": { + "sources": 211 + }, + "all_sdk_generation": "passed", + "public_rpc_schema_and_view_lifecycles": { + "passed": 22, + "sdk_count": 7, + "schema_metadata_mutation": "All seven ordinary providers reject without changing metadata or catalog versions; positive lifecycle coverage uses the durable Python reference." + }, + "source_verification": { + "repositories": 15, + "files": 33020, + "errors": 0 + }, + "final_manifest_corpus_inventory": { + "passed": 20 + } + }, + "limits": [ + "Only the durable SQLite Python reference implements and advertises the new metadata-mutation profile.", + "Other SDK record codecs and generated RPC methods are prepared; ordinary providers reject this operation without effects. Full negotiated provider behavior remains P2 work.", + "Both inspected engine pins reject COMMENT ON SCHEMA SQL. Registering the RPC enables no native SQL capability.", + "Nonempty/descendant schema replacement/drop dependency semantics remain unresolved.", + "Native 2.0 port, broader contract freeze, production transport qualification and coordinated major release remain open.", + "Previously recorded Flechette stdio limitation remains open; codec qualification does not close it." + ], + "counts_overlap": true, + "requirement_evidence": { + "Comment omission, NULL, empty string and literal case-sensitive tags": [ + "tests/catalog/test_metadata_changes.py::test_patch_retains_omission_null_empty_and_literal_tags", + "conformance/catalog-contracts.json: metadata_patch, metadata_clear_comment, metadata_noop" + ], + "Full validation before effects, including missing-target IGNORE": [ + "tests/catalog/test_metadata_changes.py", + "tests/catalog/test_schema_metadata_protocol.py::test_invalid_patch_rejects_before_provider_even_with_missing_target_ignore", + "tests/catalog/test_schema_metadata_protocol.py::test_missing_target_and_malformed_path_preserve_versions" + ], + "Atomic metadata updates preserve options; no-ops retain versions and etags": [ + "tests/catalog/test_schema_metadata_protocol.py::test_atomic_patch_noops_versions_and_snapshot_invalidation" + ], + "Transaction isolation, rollback, commit, reconnect and invalidated snapshots": [ + "tests/catalog/test_schema_metadata_protocol.py::test_patch_transaction_isolation_rollback_commit_and_reconnect" + ], + "Selected/current capability checks and sealed routing": [ + "tests/catalog/test_schema_metadata_protocol.py::test_patch_requires_current_negotiation_and_routes_sealed_handles" + ], + "DDL transaction guarantees and transaction ownership": [ + "tests/catalog/test_schema_metadata_protocol.py::test_patch_rejects_without_transaction_ddl_and_foreign_transaction" + ], + "Unqualified providers reject without effects": [ + "tests/catalog/test_view_metadata_cross_sdk.py::test_unqualified_provider_rejects_schema_metadata_mutation" + ], + "Matching SDK schemas and actual codec round trips": [ + "scripts/regen_generated.py --check --require-all", + "Per-SDK catalog contract tests using the three shared metadata patch vectors" + ], + "Native 1.5 registration and existing schema metadata regression coverage": [ + "test/sql/integration/reflection/vgi_v2_hash.test", + "test/cpp/test_schema_metadata.cpp" + ] + }, + "logs": [ + "logs/schema-alter-native.log", + "logs/schema-alter-python.log", + "logs/schema-alter-cross-sdk.log", + "logs/schema-alter-go.log", + "logs/schema-alter-rust.log", + "logs/schema-alter-ts.log", + "logs/schema-alter-java.log", + "logs/schema-alter-csharp.log", + "logs/schema-alter-cpp.log", + "logs/schema-alter-flechette.log", + "logs/schema-alter-native-reference.log", + "logs/schema-alter-final-verification.log", + "logs/schema-alter-native-python.log", + "logs/schema-alter-native-go.log", + "logs/schema-alter-native-rust.log", + "logs/schema-alter-native-typescript.log", + "logs/schema-alter-native-java.log", + "logs/schema-alter-native-csharp.log", + "logs/schema-alter-native-cpp.log", + "logs/schema-alter-committed-verification.log" + ], + "source_inventory": "state/sources.json and state/source-verification.json in the unique EC2 workspace record committed revisions and source bytes.", + "native_revision": "4365cd1ff82c11f6824edbded7ca6ff9fbc7ba18", + "python_base_revision": "1e63e89d243b165ed357dcb731ca22e7e660e5fb", + "sdk_revisions": { + "go": "216a0171b7d2bae59bb282cac9ede4f14dd295bd", + "rust": "5b5506c730c52c841591bfb2a8eb39a7107980dc", + "typescript": "97556e619ec7e8804c4d41838415e57ccafa03be", + "java": "9b043369555dd1c59153ab48f2fee0b2234417a4", + "csharp": "135f12867ee82b3ae2b811cd6eb5a4e88c1c134d", + "cpp": "ada9c1394b84ab62d203fccdc30593c6f105837f" + } + }, + "dependency_lifecycle_validation": { + "status": "reference_and_shared_codecs_qualified_on_duckdb_15", + "engine_revision": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "haybarn_2_source_revision": "e1784fa4231072d7152b8aaa20977bdb8ff4ace3", + "protocol_method_count": 80, + "protocol_hash": "e23f22d5fcbbc06ad977a3719414ed8a741b2ddf268ab0d536807611e73182fe", + "ec2_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "contract": [ + "Raw canonical attachment-relative identities preserve schema components, object kind and trigger owner; no client aliases or engine IDs.", + "Complete versioned graph reads require selected/current support, authenticate attachment and transaction ownership, and do not reserve later mutation rights.", + "RESTRICT computes owned closure before checking blockers; CASCADE follows all edges, handles reference cycles, and authorizes every affected object before effects.", + "Schema REPLACE is an atomic non-cascading drop/create; descendants and dependent objects block it.", + "Provider state, metadata and versions survive malformed requests, authorization errors and injected failures after the SQLite UPDATE.", + "Independent clients, durable transaction snapshots, reconnects, rollback, commit and concurrent-write conflicts retain one authoritative graph.", + "Mutable native worker transactions own their schema/object caches; committed catalog_contents cannot seed them; commit/rollback retain bound entries without sharing snapshot metadata.", + "Native schema/table/view CASCADE invalidates caches across schemas; nontransactional workers and catalog_version_frozen attachments retain shared catalog caching." + ], + "checks": { + "python_catalog_and_codegen": { + "passed": 844, + "skipped": 4, + "skip_reason": "External-worker parametrizations require VGI_METADATA_WORKERS; the separately configured run passes all 29 checks." + }, + "public_rpc_cross_sdk": { + "passed": 29, + "sdk_count": 7 + }, + "native_schema_lifecycle": { + "test_cases": 1, + "assertions": 42 + }, + "mypy": { + "sources": 212 + }, + "ruff": "passed", + "native_build": "passed", + "native_reference": { + "test_cases": 3, + "assertions": 218 + }, + "go": { + "packages": 13, + "failures": 0 + }, + "rust": { + "passed": 488, + "failed": 0, + "ignored": 4 + }, + "typescript": { + "passed": 608, + "skipped": 68, + "bundle_entries": 4 + }, + "java": { + "passed": 664, + "skipped": 2, + "failures": 0, + "errors": 0 + }, + "csharp": { + "passed": 608, + "failed": 0 + }, + "cpp": { + "ctests_passed": 68 + }, + "flechette_codec": { + "passed": 10 + }, + "all_sdk_generation": "passed", + "manifest_corpus_inventory_registry": { + "passed": 53 + }, + "native_schema_create_copy_cache": { + "sdk_count": 7, + "test_cases_per_sdk": 1, + "assertions_per_sdk": 99 + }, + "native_full_suites": { + "python": { + "passed": 344, + "skipped": 33, + "additional_crash_checks": 2 + }, + "go": { + "passed": 338, + "skipped": 40 + }, + "rust": { + "passed": 325, + "skipped": 42 + }, + "typescript": { + "passed": 330, + "skipped": 46 + }, + "java": { + "passed": 324, + "skipped": 37, + "additional_crash_checks": 2 + }, + "csharp": { + "passed": 330, + "skipped": 37 + }, + "cpp": { + "test_cases": 326, + "assertions": 12100, + "skipped": 47, + "additional_bearer_cases": 2, + "additional_bearer_assertions": 13 + } + }, + "source_verification": { + "repositories": 15, + "files": 33030, + "errors": 0 + } + }, + "requirement_evidence": { + "Raw identities and malformed graph validation": [ + "tests/catalog/test_dependencies.py", + "conformance/catalog-contracts.json: object_ref, trigger_ref, object_dependency, dependency_snapshot, empty_dependency_snapshot" + ], + "Atomic RESTRICT/REPLACE/CASCADE including outside-schema views": [ + "tests/catalog/test_dependencies_protocol.py::test_restrict_replace_cascade_and_literal_paths_are_atomic" + ], + "Transaction lifecycle and reconnect": [ + "tests/catalog/test_dependencies_protocol.py::test_transaction_visibility_rollback_commit_and_reconnect" + ], + "Authoritative recheck, concurrent writers and read-only snapshots": [ + "tests/catalog/test_dependencies_protocol.py::test_discovery_does_not_reserve_or_authorize_a_later_drop" + ], + "Authorization and rollback of a failure after storage mutation": [ + "tests/catalog/test_dependencies_protocol.py::test_failed_cascade_preserves_all_metadata_and_versions" + ], + "Sealed routing, capability withdrawal and transaction ownership": [ + "tests/catalog/test_dependencies_protocol.py::test_capability_lifecycle_guarantees_ownership_and_routing" + ], + "Unsupported SDK providers": [ + "tests/catalog/test_view_metadata_cross_sdk.py::test_unqualified_provider_rejects_dependency_discovery" + ], + "Native transaction-local schema visibility and concurrent isolation": [ + "native:test/sql/duckdb2/schema_dependencies.test", + "native:test/run_dependencies.py", + "native:make test_dependencies" + ], + "Upstream nested replacement/drop semantics": [ + "haybarn2:src/parser/parsed_data/create_schema_info.cpp", + "haybarn2:src/catalog/duck_catalog.cpp", + "haybarn2:test/sql/catalog/nested_schema/drop_nested_schema.test" + ], + "Frozen catalog metadata and transaction-scoped inventory reuse": [ + "native:test/sql/integration/catalog/catalog_contents.test", + "native:test/sql/integration/catalog/catalog_contents_fallback.test", + "native:test/sql/integration/macro/macros.test" + ], + "Canonical parent spelling under case-insensitive lookup": [ + "tests/catalog/test_dependencies_protocol.py::test_resolved_graph_paths_use_the_owning_schema_spelling" + ] + }, + "limits": [ + "Only the durable Python reference advertises dependency discovery and the new schema lifecycle guarantee; SDK schemas/codecs alone do not qualify provider enforcement.", + "The reference persists metadata-only tables and binds only SELECT * FROM one relation. Broader SQL, query execution and table data writes are not advertised.", + "Owned-object planning is proven through shared-record tests; production indexes, triggers, macros, constraints, object renames and cross-attachment dependencies remain open.", + "Native 2.0, browser/transport qualification, full contract review/freeze, coordinated major release and distribution remain open.", + "Mutable transaction-owned caches avoid cross-transaction reuse and may increase metadata fetches; frozen metadata retains the shared cache. Performance and retained-entry lifetime qualification remain required.", + "Previously recorded Flechette stdio limitation remains open; separate codec tests are not transport qualification." + ], + "counts_overlap": true, + "logs": [ + "logs/dependencies-python.log", + "logs/dependencies-go.log", + "logs/dependencies-rust.log", + "logs/dependencies-ts.log", + "logs/dependencies-java.log", + "logs/dependencies-csharp.log", + "logs/dependencies-cpp.log", + "logs/dependencies-cross-sdk.log", + "logs/dependencies-flechette.log", + "logs/dependencies-final-verification.log", + "logs/dependencies-final-build.log", + "logs/dependencies-final-native-schema-metadata.log", + "logs/dependencies-final-native-matrix-a.log", + "logs/dependencies-final-native-matrix-b.log", + "logs/dependencies-final-native-python.log", + "logs/dependencies-final-native-go.log", + "logs/dependencies-final-native-rust.log", + "logs/dependencies-final-native-ts.log", + "logs/dependencies-final-native-java.log", + "logs/dependencies-final-native-csharp.log", + "logs/dependencies-final-native-cpp.log", + "logs/dependencies-final-native-ts-detail.log", + "state/dependencies-final-frozen-cpp-native/run.log", + "logs/dependencies-final-canonical-validation.log", + "logs/dependencies-final-generation.log" + ], + "diagnostics": "Earlier dependencies-native-* and dependencies-before-frozen-native-* runs include failures or canceled runs. They are diagnostic evidence only; qualification uses final logs listed here.", + "source_inventory": "state/sources.json and state/source-verification.json in the unique EC2 workspace record committed revisions and source bytes.", + "native_revision": "964f1987265be5c23cc4028c54e153b6aef076b9", + "python_base_revision": "5a42fbe6ac7972c6fb9306f3d4e16661b1e550d3", + "sdk_revisions": { + "go": "d56848ac05c32a3b962fa5fc82c85c2355f7462c", + "rust": "d76a65372e7a49fcb4ce1b67dec906d0fa16b008", + "typescript": "f64abf2ddaf41171cfa3b629bc718d83dfe99140", + "java": "7120e1cb0f5d18a82af2f020eb1e636be656cb71", + "csharp": "0f3f04f7a8f252d912433bf5ef92f4ad5f521ad8", + "cpp": "585bed91b19c8774b005a2445bedc863030810ee" + } + }, + "query_preparation_validation": { + "status": "partial_protocol_implementation_on_15", + "protocol_method_count": 82, + "development_protocol_hash": "28cf2ae30b6cf07dbd4db602e4972611319b993fb073b27de456b6fcf341dc00", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "qualification_scope": "Shared SDK codec coverage and Python SDK lifecycle with a finite durable reference; no native query adapter or general SQL provider is qualified.", + "python_catalog_and_registry": { + "passed": 949, + "skipped": 4 + }, + "generator_manifest_lint_types": { + "passed": 228, + "strict_generation": "--check --require-all passed", + "ruff": "passed", + "mypy_sources": 216 + }, + "all_sdk_suites": "Python, Go, Rust, TypeScript, Java, C#, C++ suites passed; native 1.5 build passed. Later parameter-corpus checks exposed a TypeScript serialization issue; its fix passed both complete Arrow and Flechette backend suites and build/bundle checks.", + "native_regression": { + "status": "initial_full_runs_plus_targeted_fix_validation", + "scope": "Native DuckDB 1.5 integration against all seven workers. Every initial full run had one stale expected protocol hash failure; the corrected reflection test subsequently passed against all seven. Full suites were not rerun after that correction or the later TypeScript parameter codec fix.", + "initial_full_runs": { + "python": { + "passed": 343, + "skipped": 33, + "failed": 1, + "log": "logs/query-native-python.log" + }, + "go": { + "passed": 337, + "skipped": 40, + "failed": 1, + "log": "logs/query-native-go.log" + }, + "rust": { + "passed": 324, + "skipped": 42, + "failed": 1, + "log": "logs/query-native-rust.log" + }, + "typescript": { + "passed": 329, + "skipped": 46, + "failed": 1, + "log": "logs/query-native-ts-detail.log" + }, + "java": { + "passed": 323, + "skipped": 37, + "failed": 1, + "log": "logs/query-native-java.log" + }, + "csharp": { + "passed": 329, + "skipped": 37, + "failed": 1, + "log": "logs/query-native-csharp.log" + }, + "cpp": { + "passed": 325, + "skipped": 47, + "failed": 1, + "log": "state/query-cpp-native/run.log", + "counting": "The Catch summary includes 47 skipped cases in 372 reported passes; 325 cases actually executed and passed." + } + }, + "reflection_fix": { + "passed_cases": 7, + "assertions_per_worker": 2, + "log": "logs/query-parameters-reflection.log" + }, + "cpp_bearer_auth": { + "passed_cases": 2, + "passed_assertions": 13, + "log": "logs/query-native-cpp.log" + }, + "skips": "Environment-gated tests that executed no assertions are not counted as passes.", + "additional_crash_recovery": { + "python": { + "passed_cases": 2, + "log": "logs/query-checkpoint-python-crash.log" + }, + "java": { + "passed_cases": 2, + "log": "logs/query-checkpoint-java-crash.log" + } + } + }, + "remaining": [ + "Native 1.5 explicit-query adapter and typed parameter SQL binding", + "Native 2.0 optimizer/exporter integration and differential automatic-query qualification", + "Broader providers: logical-catalog policy, mutable snapshots, types and production cleanup", + "All broader P0-P3 and M0-M4 gates, combined freeze/version/routing/migration/release" + ], + "logs": [ + "logs/query-sdk-python-second.log", + "logs/query-sdk-go-second.log", + "logs/query-sdk-rust-second.log", + "logs/query-sdk-ts-second.log", + "logs/query-sdk-java-second.log", + "logs/query-sdk-csharp-second.log", + "logs/query-sdk-cpp-second.log", + "logs/query-sdk-native-first.log", + "logs/query-generation-final.log", + "logs/query-cross-sdk-rpc-final.log", + "logs/query-native-api-python.log", + "logs/query-native-api-go.log", + "logs/query-native-api-rust.log", + "logs/query-native-api-typescript.log", + "logs/query-native-api-java.log", + "logs/query-native-api-csharp.log", + "logs/query-native-api-cpp.log", + "logs/query-native-python.log", + "logs/query-native-go.log", + "logs/query-native-rust.log", + "logs/query-native-ts-detail.log", + "logs/query-native-java.log", + "logs/query-native-csharp.log", + "logs/query-native-cpp.log", + "state/query-cpp-native/run.log", + "logs/query-parameters-reflection.log", + "logs/query-parameters-python.log", + "logs/query-parameters-go.log", + "logs/query-parameters-rust.log", + "logs/query-parameters-java.log", + "logs/query-parameters-csharp.log", + "logs/query-parameters-cpp.log", + "logs/query-checkpoint-ts-arrow-full.log", + "logs/query-checkpoint-ts-flechette-full.log", + "logs/query-checkpoint-ts-build.log", + "logs/query-checkpoint-ts-flechette-bundle.log", + "logs/query-checkpoint-python-crash.log", + "logs/query-checkpoint-java-crash.log" + ], + "source_inventory": "Verified 33,044 source files across 15 repositories with no differences. state/sources.json and state/source-verification.json record exact synchronized source bytes and revisions; committed heads are verified again after the checkpoint commits.", + "rust_sdk": { + "passed": 489, + "ignored": 4, + "log": "logs/query-sdk-rust-second.log" + }, + "cross_sdk_rpc": { + "passed": 36, + "workers": [ + "python", + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "log": "logs/query-cross-sdk-rpc-final.log" + }, + "native_catalog_api": { + "assertions_per_worker": 99, + "cases_per_worker": 1, + "workers": [ + "python", + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "log_pattern": "logs/query-native-api-{worker}.log" + }, + "parameter_batch_corpus": { + "scope": "Shared IPC vectors preserve nested schema metadata, typed NULLs and an explicit one-row zero-column batch.", + "python": { + "passed": 7, + "log": "logs/query-parameters-python.log", + "note": "This later focused corpus run includes one test added after the 949-pass Python lane; it is not a replacement full-suite count." + }, + "go": { + "status": "passed generated-package tests", + "log": "logs/query-parameters-go.log" + }, + "rust": { + "passed": 1, + "log": "logs/query-parameters-rust.log" + }, + "java": { + "status": "focused Gradle test task passed", + "log": "logs/query-parameters-java.log" + }, + "csharp": { + "passed": 12, + "log": "logs/query-parameters-csharp.log" + }, + "cpp": { + "passed_ctests": 9, + "log": "logs/query-parameters-cpp.log" + }, + "typescript": { + "status": "fixed and validated against both backends", + "arrow_full_suite": { + "passed": 617, + "skipped": 68, + "log": "logs/query-checkpoint-ts-arrow-full.log" + }, + "flechette_full_suite": { + "passed": 618, + "skipped": 67, + "log": "logs/query-checkpoint-ts-flechette-full.log" + }, + "declarations_and_bundles": "passed; all four bundle entries import cleanly", + "build_log": "logs/query-checkpoint-ts-build.log", + "flechette_browser_bundle_log": "logs/query-checkpoint-ts-flechette-bundle.log", + "maintenance": "The zero-column batch carrier uses a narrow Flechette 2.4.1 internal encodeIPC import. Both backend suites and an explicit browser bundle cover this dependency; revalidate it when upgrading Flechette.", + "diagnostic_log": "logs/query-parameters-ts.log" + } + }, + "checkpoint": { + "status": "validated_checkpoint", + "pending": [], + "next_checkpoint": { + "scope": "Native DuckDB 1.5 explicit vgi_query adapter using the prepared read-query contract", + "acceptance": [ + "Opt-in capability negotiation with ordinary workers still selecting no query profile", + "Prepare and execute typed named/positional parameter batches without substituting values into SQL", + "Validate exact result schema and ordering; enforce cancellation and token cleanup", + "Focused native integration with the finite durable Python reference, including typed NULL/rebinding and error paths" + ], + "excludes": [ + "Automatic DuckDB 2.0 optimizer integration", + "General SQL provider qualification", + "Protocol publication or version freeze" + ] + }, + "review": "Read-only lifecycle, authorization, schema and continuation review found no commit blockers; production qualification remains outside this checkpoint.", + "revisions": { + "go": "636a8ab99842fb3b65fd9bfe49661063e748f046", + "rust": "55dfc0448876395fb7fc1e2de7f6e4092c79a831", + "typescript": "d8a7507796f37dc650ce2106947432b364f64ae6", + "java": "ad060adc8a76264977ca45569610e0625eb82fd7", + "csharp": "3d82a2b735cb9257852af11e75a58eb2ef8494d4", + "cpp": "d14efe73144cb5949f644ab4b797071af8084cc8", + "native": "632096327fe82c220adea7d672dd51fee943205c" + }, + "final_consistency": { + "strict_generation": "--check --require-all passed", + "generation_manifest_and_corpus": "240 passed initially; one plan/manifest title mismatch corrected; all 11 manifest checks then passed", + "ruff": "passed", + "mypy_sources": 216, + "source_verification": { + "repositories": 15, + "files": 33044, + "errors": 0 + }, + "logs": [ + "logs/query-checkpoint-final-validation.log", + "logs/query-checkpoint-lint-types.log", + "logs/query-checkpoint-manifest-final.log" + ] + }, + "python_revision": "The commit containing this manifest; exact HEAD is recorded in the synchronized source inventory." + }, + "superseded_scope": "Historical SDK/preparation checkpoint. The native 1.5 explicit adapter listed here as remaining is implemented and separately qualified by native_query_adapter_validation; preserve these original counts and limits." + }, + "types_macros_validation": { + "status": "historical_checkpoint_variant_encoding_superseded", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "protocol_method_count": 82, + "development_protocol_hash": "28cf2ae30b6cf07dbd4db602e4972611319b993fb073b27de456b6fcf341dc00", + "hash_scope": "Embedded MacroInfo/MacroCreateRequest changes do not alter the outer reflection hash. Required generation parity and shared codec vectors verify their layouts.", + "python": { + "passed": 1077, + "skipped": 5, + "log": "logs/types-macros-python-complete.log" + }, + "generator_and_logical_checks": { + "passed": 308, + "counts_overlap": true, + "strict_generation": "passed --check --require-all", + "ruff": "passed", + "mypy_sources": 218 + }, + "sdk_suites": { + "go": "all packages passed", + "rust": { + "passed": 492, + "ignored": 4 + }, + "typescript_arrow": { + "passed": 621, + "skipped": 68 + }, + "typescript_flechette": { + "passed": 622, + "skipped": 67 + }, + "java": "full test suite and worker distribution passed", + "csharp": { + "passed": 630 + }, + "cpp": { + "passed": 68 + } + }, + "shared_corpus": "14 new vectors in all seven SDKs; logical tuple/struct identity and four NULL/absence cases; declared/untyped macro parameters, literal/expression defaults, complete and legacy parent records.", + "native_build": "Final DuckDB 1.5 static/loadable extension and test targets passed.", + "qualification_limits": [ + "Native DuckDB 2.0 import/export and macro family materialization/DDL remain unqualified.", + "Production SQL owner binding, durable family mutations, cross-SDK logical execution semantics and performance remain open.", + "No version freeze, protocol publication, native engine upgrade or broad phase completion." + ], + "sdk_revisions": { + "go": "aa133e4c751798d30e8aada06291c622ff5067fa", + "rust": "5c9aa03c35bbf2fecaac5345d45b0e011613a45c", + "typescript": "132b0f9800e454896b15f6fac8a59c84685786dd", + "java": "28dc31eb661306c3e2001421696319bc0c1a1455", + "csharp": "248509d2d2cf55a91c7389ddbecaf9b3cf2cd072", + "cpp": "30f20037f7f91b64aba2b58c746312ef048d8d6f" + }, + "logs": [ + "logs/types-macros-python-complete.log", + "logs/types-macros-go-final.log", + "logs/types-macros-rust-final.log", + "logs/types-macros-ts-final.log", + "logs/types-macros-ts-focused.log", + "logs/types-macros-flechette-final.log", + "logs/types-macros-java-complete.log", + "logs/types-macros-csharp.log", + "logs/types-macros-cpp.log", + "logs/types-macros-native-final.log", + "logs/types-macros-native-guards.log", + "logs/types-macros-native-matrix.log" + ], + "diagnostics": "Earlier logs without final/complete suffix include integration failures subsequently corrected; do not treat them as final qualification.", + "native_revision": "00d336d1dfa55b2bb386b146bd46d4436f072ef0", + "native_guards": { + "cases": 3, + "assertions": 14, + "log": "logs/types-macros-native-guards.log" + }, + "native_macro_catalog_regression": { + "workers": [ + "python", + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "cases_per_worker": 7, + "assertions_per_worker": 420, + "log": "logs/types-macros-native-matrix.log" + }, + "source_inventory": "Final committed revisions and bytes recorded in state/sources.json and state/source-verification.json; checked after commit.", + "superseded_scope": "The VARIANT encoding in this tested snapshot was replaced at user direction by canonical arrow.parquet.variant. Preserve these counts/revisions as historical; they do not validate the replacement. Macro and TUPLE work remains applicable." + }, + "canonical_variant_validation": { + "status": "canonical_variant_transport_validated_native_adapters_pending", + "profile": "vgi.type.variant.parquet.v1", + "extension": "arrow.parquet.variant", + "initial_scope": "Unshredded canonical metadata/value struct; opaque byte/metadata/validity preservation; no per-cell IPC or custom scalar encodings.", + "pyarrow": "25.0.1; opaque struct/binary transport without a public Variant API", + "qualification_limits": [ + "Binary semantic validation requires a qualified backend decoder.", + "Canonical Variant semantics do not preserve every original DuckDB logical type or integer width/signedness. Standard unsigned widening is allowed; unsupported mappings reject.", + "Native DuckDB 2.0 mappings, shredded storage, cross-SDK execution and performance remain unqualified." + ], + "validation": { + "python": { + "passed": 1024, + "skipped": 5, + "scope": "tests/catalog, tests/test_logical_types.py, tests/test_catalog_contract_fixtures.py", + "ruff": "pass", + "mypy": "218 source files pass", + "generation": "strict check with all SDK roots passes", + "manifest": "151 audit rows and 20 contract areas; 134 reviews remain", + "log": "logs/canonical-variant-python.log" + }, + "go": { + "scope": "vgi/generated package", + "status": "pass", + "log": "logs/canonical-variant-go.log" + }, + "rust": { + "scope": "catalog_contracts", + "passed": 14, + "log": "logs/canonical-variant-rust.log" + }, + "typescript": { + "scope": "catalog-contracts plus build:types", + "arrow_passed": 14, + "flechette_passed": 14, + "typecheck": "pass", + "log": "logs/canonical-variant-ts.log" + }, + "java": { + "scope": "CatalogContractsTest", + "passed": 13, + "log": "logs/canonical-variant-java.log" + }, + "csharp": { + "scope": "CatalogContractsTests", + "passed": 19, + "log": "logs/canonical-variant-csharp.log" + }, + "cpp": { + "scope": "Python catalog records survive the C++ worker codec", + "ctest_passed": 1, + "log": "logs/canonical-variant-cpp.log" + }, + "native_15": { + "scope": "incremental vgi_catalog_tests build and wire-contract-guards", + "cases": 3, + "assertions": 16, + "log": "logs/canonical-variant-native.log" + } + }, + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "source_inventory": "state/sources.json records exact repository revisions and source hashes; state/source-verification.json verifies 33,056 files across 15 repositories with zero mismatches. Final committed heads are synchronized after recording this evidence." + }, + "native_query_adapter_validation": { + "status": "native_15_explicit_adapter_validated_against_finite_reference", + "date": "2026-10-14", + "native_revision": "733b546ca21f81a14b46fcdbf7168031eb3f336f", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "platform": "Linux ARM64, native subprocess transport", + "protocol_method_count": 82, + "development_protocol_hash": "28cf2ae30b6cf07dbd4db602e4972611319b993fb073b27de456b6fcf341dc00", + "wire_delta": "None: existing query preparation/executor and catalog_detach contracts.", + "behavior": [ + "Opt-in vgi_query_enabled before ATTACH; immutable explicit duckdb.sql.v1 / vgi.remote.readonly.v1 selection.", + "Typed named/positional one-row Arrow parameter batches, typed NULLs and provider-default context digest.", + "Fresh binding per DuckDB prepared statement execution; attachment and transaction ownership preserved.", + "Exact schema/metadata and required-value checks, including empty output; one ordered reader without caching or scan pushdown.", + "No ambiguous prepare/init retries; independent preparation release and execution cancellation.", + "Native DETACH invokes the existing authenticated catalog_detach RPC best effort; lost clients still require provider expiry/janitor cleanup." + ], + "native_build": { + "status": "pass", + "targets": [ + "shell", + "vgi_loadable_extension", + "vgi_catalog_tests" + ], + "log": "logs/native-query-adapter-build-final.log" + }, + "native_unit": { + "cases": 12, + "assertions": 73, + "filter": "[query-rpc],[wire-contract-guards]", + "log": "logs/native-query-adapter-native-final.log" + }, + "native_integration": { + "passed": 18, + "command": "make test_queries", + "cases": [ + "values", + "ordered_empty", + "prepared_autocommit", + "prepared_transaction", + "prepare_only", + "not_offered", + "provider_disabled", + "declined", + "missing_parameter", + "unsupported_parameter", + "prepare_error", + "prepare_disconnect", + "start_error", + "read_error", + "output_schema", + "rollback", + "early_limit", + "interrupt" + ], + "assertions": "Every scenario checks execution counts and zero retained preparations, executions, fragments and attachments. prepare_disconnect checks exactly one preparation attempt/allocation. Interruption uses Connection::Interrupt(), followed by explicit DETACH while the connection remains alive.", + "log": "logs/native-query-adapter-native-final.log" + }, + "python": { + "passed": 104, + "scope": "test_query_parameters.py, test_query_contract.py, test_query_protocol.py", + "ruff": "vgi and tests/catalog pass; final fixture changes checked separately", + "mypy_sources": 218, + "logs": [ + "logs/native-query-adapter-python.log", + "logs/native-query-adapter-fixture-final.log" + ] + }, + "native_catalog_metadata_regression": { + "workers": [ + "python", + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "cases_per_worker": 1, + "assertions_per_worker": 99, + "logs": "logs/native-query-adapter-metadata-{worker}.log" + }, + "generation_and_inventory": { + "status": "strict --check --require-all, settings generation and manifest checks pass", + "audit_rows": 151, + "contract_areas": 20, + "remaining_reviews": 134, + "log": "logs/native-query-adapter-contract-final.log" + }, + "review": "Two cross-reviews of native adapter/lifecycle and RPC/negotiation. The sole remaining blocker, inherited pooled prepare retry on ambiguous IO failure, was fixed and covered by prepare_disconnect.", + "diagnostics": "Earlier native/fast/build-interrupt logs include diagnostic failures. Missing native catalog_detach notification was fixed. The initial CLI SIGINT case exited batch input before DETACH; it was replaced with direct connection interruption without weakening resource assertions. Final qualification uses the logs named above.", + "qualification_limits": [ + "Finite durable Python reference only; no general SQL/provider, mutable snapshot or production janitor qualification.", + "Native HTTP/WASM, other platforms, automatic DuckDB 2.0 integration and performance remain unqualified.", + "Shared SDK wire inventory is unchanged; complete SDK/native suites were not repeated in this checkpoint.", + "Broader P0-P3 and M0-M4 gates, combined freeze/version/routing/migration and publication remain open." + ], + "source_inventory": "Verified 33,065 files across 15 repositories with zero mismatches. state/sources.json records exact synchronized revisions and source hashes; state/source-verification.json is checked again after the checkpoint commits." + }, + "combined_design_review": { + "date": "2026-10-14", + "status": "historical_design_review_partially_superseded_2026_10_15", + "source": "docs/protocol/shared-catalog-values.md#combined-expression-constraint-transaction-and-trigger-design", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "engine_20": "e1784fa4231072d7152b8aaa20977bdb8ff4ace3", + "implemented_protocol_methods": 82, + "implemented_protocol_hash": "28cf2ae30b6cf07dbd4db602e4972611319b993fb073b27de456b6fcf341dc00", + "reviewed_field_ids": [ + "audit-127", + "audit-129", + "audit-130", + "audit-135", + "audit-156", + "audit-157", + "audit-158", + "audit-174", + "audit-176", + "audit-177", + "audit-178", + "audit-179", + "audit-180", + "audit-181", + "audit-182", + "audit-217", + "audit-218", + "audit-219", + "audit-220", + "audit-221", + "audit-223", + "audit-224", + "audit-225", + "audit-241", + "audit-242", + "audit-243", + "audit-245", + "audit-300", + "audit-323", + "audit-324", + "audit-325", + "audit-326", + "audit-327", + "audit-348", + "audit-350", + "audit-381", + "audit-382", + "audit-383", + "audit-384", + "audit-385", + "audit-386", + "audit-387", + "audit-388", + "audit-412", + "audit-415", + "audit-414" + ], + "reconciled_existing_work": [ + "Native 1.5 explicit vgi_query checkpoint", + "Implemented catalog methods moved from proposed to existing inventory", + "Macro prototype targets corrected to actual parameter records" + ], + "qualification_limits": [ + "No new runtime records, methods or profiles implemented by this design checkpoint.", + "At the time of this historical review, proposed statement/trigger/replay runtime proof was absent. The statement/replay framework is now out of scope; trigger execution must first undergo the existing-write-path gap audit.", + "Unreviewed areas/candidates remain in the manifest; no broad release/native gate is complete." + ], + "validation": { + "status": "remote_inventory_and_generation_checks_passed", + "manifest_tests_passed": 11, + "audit_rows": 151, + "contract_areas": 20, + "previous_pending_reviews": 134, + "remaining_pending_reviews": 84, + "resolved_review_entries": 50, + "strict_generation": "--check --require-all passed; runtime inventory unchanged", + "source_verification": { + "repositories": 15, + "files": 33065, + "mismatches": 0 + }, + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "log": "logs/combined-protocol-design-checkpoint.log", + "limits": "Inventory/consistency checks only. No new runtime or wire implementation was introduced or qualified." + }, + "scope_superseded_by": "write_scope_decision_2026_10_15", + "historical_newly_specified_method_count": 12, + "historical_newly_specified_methods": [ + "catalog_table_constraints_apply", + "catalog_index_option_definitions", + "catalog_table_triggers", + "catalog_trigger_get", + "catalog_trigger_create", + "catalog_trigger_drop", + "catalog_trigger_alter_metadata", + "catalog_write_statement_begin", + "catalog_write_statement_complete", + "catalog_write_statement_abort", + "catalog_mutation_execute", + "catalog_operation_status" + ], + "historical_review_findings_resolved": [ + "Singular trigger event and one-component UPDATE OF at the engine pin", + "Stable constraint refs distinct from optional SQL labels", + "Column-scoped dependency identity and cycle checks", + "Explicit table definition revision lifecycle and CREATE enforcement", + "Statement terminal-state rules, COMMIT fence and no stream reinitialization", + "Retained replay receipts remain recoverable after transaction completion", + "Authority selects UPDATE OF using immutable SET paths and validates exact trigger ref/revision per intent", + "Operation receipts use final empty-batch IPC metadata; zero-row completion and one stream per operation are explicit", + "Canonical predecessor completion gates BEFORE/base/AFTER initialization" + ], + "current_applicability": "The method list, findings and validation counts record the 2026-10-14 design review, not the active package. The 2026-10-15 user decision supersedes its generic statement lifecycle, durable replay/status and savepoint assumptions. Runtime checkpoint tests and unrelated SQL/constraint/index decisions are retained. The 2026-10-23 combined review also defers new trigger CRUD/execution and reconciles all active area/field mappings; historical evidence remains unchanged." + }, + "expression_constraint_checkpoint": { + "date": "2026-10-14", + "status": "records_generated_bounded_reference_verified_on_15", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "implemented_records": [ + "SqlExpression", + "SqlStatement", + "QueryEvaluationContext", + "MacroOverload", + "ForeignKeyDefinition", + "ConstraintDefinition", + "ConstraintInfo", + "ConstraintChange", + "ConstraintSnapshot", + "ConstraintsApplyRequest" + ], + "public_method_count": 82, + "public_methods_added": [], + "column_metadata_encoding": "Canonical RFC4648 standard padded base64 ASCII of exact single-record SqlExpression IPC; generation is virtual/stored ASCII. Raw IPC metadata rejected after Rust Arrow Utf8Error. Binary record fields retain raw IPC.", + "test_evidence": [ + { + "check": "Python catalog, corpus, codegen, manifest and generation regression suite", + "result": "1350 passed; 5 pre-existing opt-in live-worker metadata cases skipped because VGI_METADATA_WORKERS was not set. SDK codec suites ran independently.", + "log": "constraints-python-verified.log" + }, + { + "check": "ruff and mypy", + "result": "passed; 221 source files type checked", + "log": "constraints-quality-verified.log" + }, + { + "check": "strict all-SDK generation, manifest inventory, native settings generation", + "result": "passed; 151 audit rows, 20 areas, 84 reviews remain", + "log": "constraints-python-verified.log" + }, + { + "check": "All generated-file checks", + "result": "244 passed, no skips; focused native header checks also pass", + "log": "constraints-generation-coverage.log" + }, + { + "check": "Go public/generated record suites", + "result": "passed, including complete schema inventory and new expression metadata", + "log": "constraints-go-final.log" + }, + { + "check": "Rust vgi-protocol suite", + "result": "53 checks passed, including 16 catalog corpus tests", + "log": "constraints-rust-final.log" + }, + { + "check": "TypeScript corpus on apache-arrow and flechette plus type build", + "result": "16 tests and 245 assertions passed per backend; type build passed", + "log": "constraints-ts-final.log" + }, + { + "check": "Java CatalogContractsTest", + "result": "passed", + "log": "constraints-java-final.log" + }, + { + "check": "C# CatalogContractsTests", + "result": "21 passed", + "log": "constraints-csharp-final.log" + }, + { + "check": "C++ catalog codec target and corpus", + "result": "build and test passed", + "log": "constraints-cpp-final.log" + }, + { + "check": "Native pinned-1.5 shell, loadable extension, guard/query tests and lifecycle scenarios", + "result": "build passed; 13 cases/98 assertions; 18 scenarios with no retained query resources", + "log": "constraints-native-final.log" + }, + { + "check": "Raw IPC in Arrow metadata interoperability probe", + "result": "rejected by Rust Arrow UTF-8 validation; replaced with canonical base64 and all-SDK exact-IPC recovery proof", + "log": "constraints-metadata-proof.log" + } + ], + "limits": [ + "Legacy TableCreateRequest/TableInfo constraint arrays and public apply RPC are unchanged.", + "Finite direct durable constraint reference; no new capability advertisement or production/native enforcement.", + "Scalar evaluation proof does not establish persisted DEFAULT/generated columns or SqlStatement execution.", + "At this checkpoint indexes and ALTER dependency scope remained pending; later checkpoints record their bounded completion. Current write/trigger follow-up follows write_scope_decision_2026_10_15, without generic statement lifecycle or durable mutation recovery.", + "No P0-P3 or M0-M4 gate closed; no version freeze/publication." + ], + "remote_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs_directory": "logs/" + }, + "constraint_public_checkpoint": { + "date": "2026-10-14", + "status": "verified_on_pinned_15_bounded_adapters", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "public_method_count": 83, + "public_methods_added": [ + "catalog_table_constraints_apply" + ], + "public_shapes": { + "TableCreateRequest": "Required constraint_definitions:list; replaces five legacy arrays.", + "TableInfo": "Required constraints:list; optional definition_revision:binary, required nonempty with selected mutation support." + }, + "scope": "Public Python/provider dispatch, generated seven-SDK contracts and fail-closed unported adapters; durable reference and native1.5 subset.", + "remaining": [ + "Native CHECK and nested constraints; persistent column-expression translation.", + "Qualified enforcement/mutation in other SDK providers; generic bind/write race authorization is not established.", + "Typed indexes and semantic ALTER dependency work are recorded by later checkpoints. Current remaining write/trigger scope is the existing-lifecycle gap audit and minimum metadata/owner/stale-bind review; generic statement lifecycle and durable mutation recovery are excluded." + ], + "test_evidence": [ + { + "check": "Python catalog, client/CLI/reflection, corpus and generated inventory suite", + "result": "1599 passed; 5 pre-existing opt-in live-worker metadata cases skipped because VGI_METADATA_WORKERS was unset.", + "log": "constraints-public-python-verified.log" + }, + { + "check": "ruff and mypy", + "result": "passed; 222 production source files type checked", + "log": "constraints-public-quality-final.log" + }, + { + "check": "Native 1.5 shell, loadable extension and codec/guard tests", + "result": "passed: 21 cases, 218 assertions", + "log": "constraints-public-native-verified.log" + }, + { + "check": "Native SQL canonical constraints", + "result": "5 scenarios passed: PK/UNIQUE/FK discovery, raw names, stable keys and revisions, SET/DROP NOT NULL, transaction read-your-writes/rollback/commit, PK-implied NOT NULL and unsupported CHECK rejection.", + "log": "constraints-public-native-verified.log" + }, + { + "check": "Native prepared query lifecycle regression", + "result": "18 scenarios passed, no retained query resources", + "log": "constraints-public-native-verified.log" + }, + { + "check": "Generated schema/registry drift and manifest coverage", + "result": "passed across all SDKs; 151 audit rows, 20 areas, 84 reviews remain", + "log": "constraints-public-python-verified.log" + }, + { + "check": "Browser client rebuild and public exports", + "result": "Rebuilt from the updated TypeScript client; all 8 established landing-page exports retained.", + "log": "constraints-public-bundle-verified.log" + }, + { + "check": "Python served browser client and HTTP endpoints", + "result": "75 passed, including execution of the shipped browser bundle against the Python server.", + "log": "constraints-public-python-smoke-checkpoint.log" + }, + { + "check": "Go SDK full suite", + "result": "go test ./... passed, including example-worker canonical discovery and rejection before replacement effects.", + "log": "constraints-public-go-complete.log" + }, + { + "check": "TypeScript SDK and package bundles", + "result": "183 passed and 62 opt-in integration cases skipped on each Arrow backend; types, all four package bundles and browser client build passed. Browser digest unchanged after final guard edits.", + "log": "constraints-public-ts-checkpoint.log" + }, + { + "check": "Java SDK full suite", + "result": "648 core SDK tests and 42 example-worker tests passed; 2 live Cloudflare Durable Object storage cases skipped.", + "log": "constraints-public-java-dictionary-verified.log" + }, + { + "check": "C# SDK full suite", + "result": "643 passed, no skips; includes example constraint discovery.", + "log": "constraints-public-csharp-complete.log" + }, + { + "check": "C++ SDK full build and suite", + "result": "69 passed, including example discovery, explicit historical constraint snapshots and rejection before replacement effects.", + "log": "constraints-public-cpp-complete-verified.log" + }, + { + "check": "Rust SDK full suite and built example worker", + "result": "492 passed, 4 documentation examples ignored. Rebuilt example worker before client integration tests; current/latest canonical facts work and unsupported historical claims reject.", + "log": "constraints-public-rust-complete.log" + }, + { + "check": "Java RPC scalar dictionary serialization", + "result": "610 passed; real int16 dictionary IPC, nullable values and strict VGI record schemas verified.", + "log": "constraints-public-java-dictionary-verified.log" + }, + { + "check": "Cross-SDK canonical discovery through built workers", + "result": "7 passed: shared discovery over Go, Rust, TypeScript, Java, C# and C++ workers, plus protocol conformance setup. Stable identities, explicit untrusted state and schema nullability checked.", + "log": "constraints-public-java-dictionary-verified.log" + } + ], + "protocol_hash": "a81e4c5fb450665c3e4991333ba8a404b6482a345269bc3df5b1c02680d71bea", + "browser_client": { + "sha256": "b913fe227d5a32b7146dfd4d34dc3911f5f6ed966742aa14e50afbd756276c63", + "source": "vgi-typescript/scripts/landing-client-entry.ts, mirroring vgi-web-frontend/src/landing/vgi-client-entry.ts", + "build": "bun run build:landing-client", + "vendored_into": [ + "Python", + "Go", + "TypeScript", + "C#", + "C++" + ] + }, + "implementation_limits": [ + "Native CHECK and nested constraint paths reject; no native default/generated-column translation.", + "Durable reference advertises catalog/DDL operations only, not scan/DML. Row enforcement proofs use its shared provider authority.", + "Non-Python mutable providers reject constrained creation/apply rather than advertising enforcement.", + "SDK example records are explicitly METADATA_ONLY/UNVALIDATED; this does not qualify native trusted-constraint materialization or complete provider profile negotiation.", + "Legacy mutation/write prechecks are not atomic provider authorization; generic bind/write lifecycle remains future work.", + "No broad DuckDB 2.0 runtime qualification or protocol publication." + ], + "review": "Independent bounded review of six SDK CREATE guards confirmed validation before replacement effects; fixed the identified C++ schema lifetime issue before the final rerun.", + "remote_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs_directory": "logs/", + "supporting_rpc_fix": { + "repository": "vgi-rpc-java", + "branch": "codex/duckdb-2.0-qualification", + "change": "Honor scalar DICT_INT16_UTF8 record annotations so canonical enum fields retain actual Arrow dictionary encoding; uses the existing dictionary-aware RecordCodec writer.", + "regression": "RecordDictionaryEncodingTest checks emitted IPC indices, dictionary values, nullable fields and ordinary strings; VGI tests require exact canonical-record IPC schemas." + } + }, + "index_public_checkpoint": { + "date": "2026-10-14", + "status": "verified_on_pinned_15_bounded_adapters", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "public_method_count": 84, + "public_methods_added": [ + "catalog_index_option_definitions" + ], + "protocol_hash": "8003abf6447266f5bc59cca76bef09df661f35fbbf0b360f400d92fdd4b12aee", + "public_shapes": { + "IndexKey": "Exactly one raw column_path or SqlExpression IPC.", + "IndexDefinition": "Required table reference, explicit algorithm, ordered key IPC records, TypedValueBatch options and NONE/UNIQUE/PRIMARY; optional authoritative constraint reference.", + "IndexInfo": "Required definition and OptionState IPC replace legacy table_name/index_type/expressions/string options.", + "IndexCreateRequest": "Required definition replaces legacy index fields; identity/conflict/transaction retained." + }, + "test_evidence": [ + { + "check": "Python catalog/client/reflection/codegen suite", + "result": "1643 passed; 8 opt-in worker checks skipped without VGI_METADATA_WORKERS.", + "log": "indexes-python.log" + }, + { + "check": "Python final targeted regression and strict all-SDK generation", + "result": "74 passed; all generated targets match; manifest covers151 audit rows/20 areas with84 open reviews.", + "log": "indexes-python-second.log" + }, + { + "check": "ruff and mypy", + "result": "Passed;223 production source files type checked.", + "log": "indexes-python.log" + }, + { + "check": "Native1.5 shell/loadable extension and codec/guard tests", + "result": "26 cases and401 assertions passed.", + "log": "indexes-native-second.log" + }, + { + "check": "Native typed-index SQL lifecycle", + "result": "7 scenarios passed: create/discovery/reconnect/drop, transaction rollback/commit, EXPLAIN has no effects, expression/options/default ART/nonbinary collation rejection.", + "log": "indexes-native-second.log" + }, + { + "check": "Native canonical-constraint regression", + "result": "5 SQL lifecycle scenarios passed.", + "log": "indexes-native-second.log" + }, + { + "check": "Go full suite and worker build", + "result": "Passed.", + "log": "indexes-go.log" + }, + { + "check": "Rust full protocol/client/worker suites", + "result": "494 passed;4 documentation examples ignored.", + "log": "indexes-rust.log" + }, + { + "check": "TypeScript both Arrow backends, types and package/browser builds", + "result": "185 passed/62 opt-in skips on each backend; all4 package entries import and browser build passes.", + "log": "indexes-typescript-second.log" + }, + { + "check": "Java core and example-worker suites", + "result": "652 core and42 example tests passed;2 live storage cases skipped.", + "log": "indexes-java-second.log" + }, + { + "check": "C# full suite", + "result": "651 passed,0 skips.", + "log": "indexes-csharp.log" + }, + { + "check": "C++ full build and suite", + "result": "69 passed.", + "log": "indexes-cpp.log" + }, + { + "check": "Live built SDK workers", + "result": "15 passed: typed discovery, unsupported create/replace/drop/options without state changes, canonical constraint discovery, conformance setup.", + "log": "indexes-cross-sdk.log" + }, + { + "check": "Native prepared query lifecycle regression", + "result": "18 scenarios passed; no retained query resources.", + "log": "indexes-native-second.log" + }, + { + "check": "Served browser client and HTTP endpoints", + "result": "75 passed; rebuilt browser bundle executes discovery against the Python server.", + "log": "indexes-browser.log" + }, + { + "check": "Native HTTP single-attempt mutation transport and interrupts", + "result": "5 cases/145 assertions passed. Protected calls sent exactly one POST after500, codec-negotiation500 or lost reply; readonly calls retain retries and cached parameters remain unchanged.", + "log": "indexes-http-final.log" + }, + { + "check": "Packages rebuilt with updated vendored browser client", + "result": "TypeScript package/export checks, Go build, C# build and C++ build passed.", + "log": "indexes-packages-final.log" + } + ], + "browser_client": { + "sha256": "b33f27a730c045d591dc01906a3bbdddbac174e917592fb297b9c526c54ec6cb", + "build": "bun run build:landing-client", + "vendored_into": [ + "Python", + "Go", + "TypeScript", + "C#", + "C++" + ] + }, + "implementation_limits": [ + "Native/reference regular and UNIQUE raw top-level keys with binary collation and explicit SQLITE.BTREE.V1. Rich typed options, expression/nested keys and PRIMARY association have wire coverage but no provider qualification. No constraint-backing closure, CASCADE, general ALTER dependency lifecycle or DuckDB2 runtime proof.", + "Reference indexes are actual SQLite physical indexes; provider-row tests prove UNIQUE failures and atomic replacement. Public scan and catalog/DDL operations are exposed; public DML is not qualified.", + "Other mutable SDK providers explicitly refuse index DDL and option discovery. TypeScript example indexes use fixture.metadata and NONE; other SDK examples advertise none.", + "TypeScript generated binary record fields now match Uint8Array codec behavior; its handwritten catalogAttach client still omits negotiated capability fields, so it does not qualify provider negotiation.", + "Generic wire identity permits cross-schema index/table ownership; bounded native/reference providers require the same flat schema.", + "Protocol version remains an unpublished development placeholder; no engine pin, release gate, publication or broad DuckDB2 qualification changes.", + "Single-attempt subprocess/HTTP policy covers catalog_query_prepare, catalog_table_create, catalog_table_constraints_apply, catalog_index_create and catalog_index_drop. Explicit predispatch415 codec and401 auth handling remain bounded. This is not durable replay or a guarantee for all legacy mutation methods." + ], + "next": "Semantic ALTER dependencies completed in the subsequent checkpoint; current follow-up: Audit concrete DuckDB2 write/trigger gaps against existing transactions, init/exchange/finalize and OLD/NEW results; propose only necessary metadata, execution-owner and stale-bind checks. No new general statement lifecycle or recovery service.", + "remote_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs_directory": "logs/", + "review": "Subagent reviews covered generated binary-field types, refusal before effects, native cache invalidation and retry boundaries. Review found the HTTP automatic-retry gap; fixed it and proved request counts on a real loopback server." + }, + "semantic_alter_checkpoint": { + "date": "2026-10-14", + "status": "verified_on_pinned_15_bounded_metadata_reference", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "public_method_count": 84, + "public_methods_added": [], + "protocol_hash": "8003abf6447266f5bc59cca76bef09df661f35fbbf0b360f400d92fdd4b12aee", + "hash_scope": "Unchanged top-level method schemas do not include ObjectDependency fields embedded in binary payloads; this required nested field is still breaking.", + "wire_changes": { + "ObjectDependency": "Required blocks_alter bool, nullable nonempty raw subject_column_paths and nullable dependent_column_path; kind retains requires/owned DROP semantics." + }, + "test_evidence": [ + { + "check": "Python initial broad regression", + "result": "1689 passed;8 opt-in SDK checks skipped without VGI_METADATA_WORKERS.", + "log": "alter-python.log" + }, + { + "check": "Python final broad regression", + "result": "1710 passed;8 opt-in skips. A new fixture omitted required TableInfo comment/tags; corrected and covered in the final targeted regression below.", + "log": "alter-python-final.log" + }, + { + "check": "Python final semantic ALTER, negotiation, constraint and corpus regression", + "result": "157 passed; no skips.", + "log": "alter-python-verified.log" + }, + { + "check": "Ruff and mypy", + "result": "Passed;223 production source files checked.", + "log": "alter-python-final.log" + }, + { + "check": "Native1.5 shell/loadable extension and codec guards", + "result": "26 cases;401 assertions passed.", + "log": "alter-native.log" + }, + { + "check": "Native HTTP single-attempt and interrupt regression", + "result": "5 cases;241 assertions passed, including all four semantic ALTER RPCs after HTTP errors, codec fallback errors and lost replies.", + "log": "alter-native.log" + }, + { + "check": "Native semantic ALTER SQL lifecycle", + "result": "6 scenarios passed: raw names/cache refresh/reconnect, view blockers, view CASCADE, DDL transactions, no-effect TYPE conversion rejection and generated-metadata rejection.", + "log": "alter-native-verified.log" + }, + { + "check": "Native constraint/index regression", + "result": "5 constraint scenarios and7 typed-index scenarios passed.", + "log": "alter-native-verified.log" + }, + { + "check": "Go full suite and example-worker build", + "result": "Passed.", + "log": "alter-go.log" + }, + { + "check": "Rust protocol/client/worker suites", + "result": "494 passed;4 documentation examples ignored.", + "log": "alter-rust.log" + }, + { + "check": "TypeScript both Arrow backends, package/types/browser builds", + "result": "186 passed and62 opt-in skips per backend; all package entries import and browser build passes.", + "log": "alter-typescript.log" + }, + { + "check": "Java core and example-worker suites", + "result": "652 core tests passed,2 live storage skips;42 example tests passed.", + "log": "alter-java.log" + }, + { + "check": "C# full suite", + "result": "652 passed;0 skips.", + "log": "alter-csharp.log" + }, + { + "check": "C++ build and suite", + "result": "69 passed.", + "log": "alter-cpp.log" + }, + { + "check": "Live independently built SDK workers", + "result": "39 passed across all six SDK workers, including dependency discovery refusal.", + "log": "alter-public-final.log" + }, + { + "check": "Browser/HTTP discovery and serving", + "result": "75 passed.", + "log": "alter-public-final.log" + }, + { + "check": "All-SDK generation and manifest inventory", + "result": "Exact generated matches;151 audit rows/20 areas/84 pending review decisions.", + "log": "alter-python-verified.log" + } + ], + "review_fixes": [ + "Separate table column computations from whole-object lifetime edges so valid object cycles do not become false generated-column cycles; retain view-definition and ownership-cycle checks.", + "Deduplicate resolved generated references after provider case-insensitive binding, preserving canonical raw names.", + "Reject trailing Arrow schema bytes before table publication.", + "Require selected/current expression profiles for canonical field metadata, and selected/current DDL guarantees for dependency-provider transactional mutations even if the client omits dependency read." + ], + "browser_client": { + "sha256": "953a7f009d166155418a29dbffa1490540d002fe46771013e65a199e441fdc25", + "build": "bun run build:landing-client", + "vendored_into": [ + "Python", + "Go", + "TypeScript", + "C#", + "C++" + ] + }, + "implementation_limits": [ + "Reference tables are metadata-only. No row conversion, physical generated values, DML or constraint/index enforcement is claimed by this dependency reference.", + "Generated expressions are bounded top-level primitive virtual expressions with default explicit context; views are single-relation SELECT*. Unsupported grammar/contexts/storage semantics reject before effects.", + "Rename/type with dependents reject rather than rewriting SQL definitions. Ordinary nested raw-column paths are supported in the Python reference, not qualified in native2.", + "Native1.5 TYPE receives a parser-generated conversion expression even without USING and rejects in this metadata provider. Canonical generated metadata is rejected by native capability negotiation/materialization.", + "Constraint/backing-index/FK ownership and exact referenced-column closure have core planner proof only; production cross-object enforcement and trigger integration remain pending.", + "Other SDKs preserve wire fields but do not implement/advertise the dependency authority. The generic planner is advisory; providers must validate authoritative schemas and authorize/publish closures atomically.", + "Single-attempt coverage now includes table rename and column rename/type/drop; this does not qualify every legacy mutation or supply durable replay.", + "No engine pin, release version, route, publication or broad DuckDB2 qualification changes." + ], + "next": "Audit concrete DuckDB2 write/trigger gaps against existing transactions, init/exchange/finalize and OLD/NEW results; propose only necessary metadata, execution-owner and stale-bind checks. No new general statement lifecycle or recovery service.", + "remote_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs_directory": "logs/" + }, + "write_scope_decision_2026_10_15": { + "date": "2026-10-15", + "status": "user_directed_scope_correction", + "reason": "The user rejected overengineering lost-response recovery, savepoints and a new transaction framework when VGI already has transaction and streamed-write fundamentals.", + "reuse": [ + "Existing transaction handles and catalog_transaction_begin/commit/rollback.", + "Existing write bind/init/exchange/finalize and OLD/NEW changes result mode.", + "Full transaction rollback for failed writes; no partial commit of admitted base/trigger effects.", + "No automatic mutation retry after ambiguous dispatch." + ], + "excluded_from_current_package": [ + "Durable mutation replay/status service and operation outcome retention.", + "Savepoints and statement-level rollback profiles or machinery.", + "General statement ticket, intent, execution-context and receipt lifecycle framework or RPCs." + ], + "removed_proposed_methods": [ + "catalog_write_statement_begin", + "catalog_write_statement_complete", + "catalog_write_statement_abort", + "catalog_mutation_execute", + "catalog_operation_status" + ], + "remaining_review": [ + "Audit concrete DuckDB2 execution gaps against existing code before adding protocol fields or methods.", + "Keep minimum trigger metadata/discovery, one execution owner, complete dependencies and stale-bind/current-definition checks under review; candidate record names are not finalized.", + "Prove supported CTAS, expanded ALTER, multi-DML CTE and trigger behavior with current transaction/write phases; reject unsupported forms." + ], + "preserved_evidence": "All tested runtime checkpoints, protocol hashes/method counts, engine pins and audit source rows remain intact. Historical design proposals are explicitly marked superseded; unrelated prepared-read query recovery and persisted provider event replay are unchanged.", + "next": "Source audit completed in write_path_source_audit_2026_10_15; implement focused 1.5-compatible transaction cleanup, duplicate-completion and finalization checks before native2 trigger integration.", + "validation": "Manifest content updated locally only; no new runtime tests, protocol changes or release-gate completion are claimed." + }, + "write_path_source_audit_2026_10_15": { + "date": "2026-10-15", + "status": "source_audit_complete_runtime_qualification_pending", + "revisions": { + "haybarn_20": "e1784fa4231072d7152b8aaa20977bdb8ff4ace3", + "active_engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "native": "02c876af3b7509761046dd8c5703462a3a246a9a", + "python": "4e2fd83240fe9d871652eaffc2544420c37e1433" + }, + "conclusion": "Reuse existing transactions, write bind/init/exchange/finalize, engine scheduling and OLD/NEW changes. No demonstrated need for a new statement coordinator, savepoints, replay/status RPCs, receipts or mandatory per-write requirements revision.", + "engine_findings": [ + { + "finding": "Statement triggers expand into BEFORE/base/AFTER CTEs with side-effect ordering; base DML forces RETURNING *. Row triggers lower to dependent joins preserving duplicate multiplicity, with unspecified row order, all rows for A before B, and no visibility of earlier individual firings' writes. Engine scheduling can coordinate ordinary VGI writes in the provider transaction.", + "sources": [ + "src/planner/binder/query_node/bind_trigger_expansion.cpp:361", + "src/execution/operator/set/physical_cte.cpp:409", + "src/planner/planner.cpp:80", + "src/include/duckdb/planner/operator/logical_trigger.hpp:25" + ] + }, + { + "finding": "Expanded ALTER uses the existing transaction preprocessor and rollback/invalidation machinery. Transaction grammar offers begin/commit/rollback, not savepoints. Multi-DML CTEs require a catalog opt-in that VGI has not enabled.", + "sources": [ + "src/planner/statement_preprocessor.cpp:32", + "src/main/client_context_execution.cpp:82", + "src/main/client_context.cpp:1084", + "src/parser/peg/grammar/statements/transaction.gram:1", + "src/include/duckdb/parser/parsed_data/transaction_info.hpp:15", + "src/planner/binder/query_node/bind_cte_node.cpp:92" + ] + }, + { + "finding": "VGI has no GetCatalogVersion override; the absent engine version causes conservative prepared-statement rebind. This does not prove provider cache/snapshot concurrency, but removes stale native plans alone as justification for a new requirements revision.", + "sources": [ + "src/include/duckdb/catalog/catalog.hpp:146", + "src/main/prepared_statement_data.cpp:28" + ] + }, + { + "finding": "External table Create/Scan/Get/DropTrigger hooks are available. ScanTriggers supplies both native discovery and executable selection without ownership filtering, and must preserve case-insensitive alphabetical order. Start with ENGINE-owned execution and provider-persisted definitions; native BACKEND-only discovery needs an engine filter.", + "sources": [ + "src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp:160", + "src/catalog/catalog_entry/table_catalog_entry.cpp:388", + "src/function/table/system/duckdb_triggers.cpp:83" + ] + }, + { + "finding": "UPDATE OLD capture expects NEW then OLD columns, and duplicate-target policy matters for UPDATE FROM. The native adapter must bridge existing paired changes and honor the engine policy; it cannot zip returned rows to input OLD values without proven correspondence.", + "sources": [ + "src/planner/operator/logical_update.cpp:27", + "src/execution/operator/persistent/physical_update.cpp:124" + ] + } + ], + "existing_vgi_paths": [ + "native:src/storage/vgi_physical_write.cpp:448 creates CTAS during execution; write setup at :140 passes the provider transaction and :262 runs optional finalization.", + "vgi/write_results.py:39 already defines paired nullable OLD/NEW changes. Native writes currently request count/rows; final output is always parsed as count.", + "vgi/table_function.py:1269 and native:src/vgi_function_connection.cpp:1040 preserve per-physical-write execution identity across INPUT/FINALIZE, not a global statement identifier. Engine scheduling does not require adding one.", + "vgi/_test_fixtures/writable/generic.py:177 and vgi/transactor/server.py:628 execute backend SQL per input batch. This alone does not qualify whole-statement BACKEND trigger semantics." + ], + "first_15_compatible_checkpoint": [ + { + "status": "corrected_existing_completion_idempotency_begin_review_separate", + "finding": "The earlier blanket single-attempt recommendation overlooked the existing commit/rollback idempotency contract. These requests already carry the transaction identifier; the transactor removes a completed transaction and rejects repeats as inactive. Preserve completion retry behavior and test no repeated effects, without requiring a recovery service or successful duplicate response. BEGIN returns the newly allocated identifier, so inspect abandoned-allocation cleanup before deciding whether its retry policy needs changing.", + "sources": [ + "vgi/catalog/catalog_interface.py:1930", + "vgi/transactor/server.py:88", + "vgi/transactor/server.py:142", + "vgi/transactor/server.py:155", + "native:src/include/vgi_unary_retry.hpp:9", + "native:src/vgi_catalog_api.cpp:1235", + "native:src/vgi_unary_rpc.cpp:397" + ] + }, + { + "status": "source_confirmed_exception_path", + "finding": "Commit/rollback exceptions bypass native transaction-map erasure; commit also skips subsequent cache clearing. Make local cleanup exception-safe and test definite provider refusal. An ambiguous remote commit must still report unknown outcome rather than claim it was undone.", + "sources": [ + "native:src/storage/vgi_transaction.cpp:118", + "haybarn2:src/transaction/meta_transaction.cpp:149", + "haybarn2:src/transaction/transaction_context.cpp:62" + ] + }, + { + "status": "followup_narrowed_no_current_provider_failure_demonstrated", + "finding": "Pre-FINALIZE INPUT drain reads raw batches without handling error metadata and breaks on some non-Iroh read failures. However, the 2026-10-22 audit rules out ordinary Python emit-then-raise as a response followed by an error: locked RPC 0.50.0 buffers until process succeeds. Current writable fixtures do not enable FINALIZE. Require a concrete reachable failure before promoting this robustness observation to a fix.", + "sources": [ + "native:src/vgi_function_connection.cpp:1007", + "native:src/vgi_function_connection.cpp:1297", + "native:src/storage/vgi_physical_write.cpp:553" + ] + }, + { + "status": "candidate_requires_reproduction", + "finding": "Signed HTTP writable fixture reads the raw sealed bind transaction token and forwards it to the transactor, whose lookup expects its original token. Reproduce authenticated begin/write/commit/rollback; if confirmed, unwrap only at the authorized provider boundary while preserving sealed wire/storage state.", + "sources": [ + "vgi/worker.py:5145", + "vgi/_test_fixtures/writable/table.py:74", + "vgi/_test_fixtures/writable/generic.py:177", + "vgi/transactor/server.py:88" + ] + }, + { + "status": "optional_deferred_returning_adapter_gap", + "finding": "FinalizeWriteConnection interprets all nonempty final output as count. The API permits opt-in final output, but current generic/simple writable fixtures return results during INPUT and do not enable FINALIZE. Before enabling deferred RETURNING, add final-row handling with a concrete provider/test. This is not a reproduced current-fixture regression; native changes adaptation belongs to the DuckDB2 lane.", + "sources": [ + "native:src/storage/vgi_physical_write.cpp:262" + ] + } + ], + "qualification_cases": [ + "Autocommit and explicit transactions: init, later-batch and opted-in finalize failures cannot partially commit; include prior successful writes, zero rows, process errors, missing response and premature EOS. Ordinary Python emit-then-raise does not put the emitted response on the wire first.", + "CTAS insert/finalize failure removes both table and rows; expanded ALTER follow-up failure rolls back the full change.", + "Before enabling multi-DML or triggers, prove BEFORE/base/AFTER ordering and rollback across all admitted effects; return results only after successful operation completion.", + "OLD/NEW and cardinality: UPDATE FROM duplicate matches, DELETE, generated columns, identical rows, zero rows and multiple batches.", + "PREPARE before trigger creation/change then EXECUTE: verify rebind, transaction-visible metadata, ordering and exclusive execution owner.", + "Commit/rollback refusal clears native bookkeeping; repeated completion reuses the same transaction identifier without repeating effects. An inactive-transaction error alone does not establish the earlier outcome. Review BEGIN allocation/cleanup separately.", + "Signed HTTP begin/write/commit/rollback uses the authenticated provider transaction; reject wrong-attachment tokens." + ], + "validation_limits": [ + "Source inspection only. EC2 32.192.49.19 was unreachable; no new builds/tests or remote synchronization.", + "native:test_deferred/writable contains deferred SQL cases. tests/transactor/test_transactor.py is module-skipped; tests/conformance/test_writable.py is a skip stub. General finalization and core-engine trigger tests are not external VGI write/trigger qualification.", + "No runtime implementation, schema/method inventory change, engine-pin change, release-gate completion or publication. All earlier bounded runtime evidence remains unchanged." + ], + "next": "Implement focused 1.5-compatible transaction cleanup and duplicate-completion tests, then reproduce the signed-HTTP token candidate. Scope finalization work using the 2026-10-22 follow-up before persistent trigger CRUD, native2 ENGINE hooks and existing changes adaptation. Keep unsupported forms and multi-DML opt-in disabled until qualified." + }, + "write_finalization_source_audit_2026_10_22": { + "date": "2026-10-22", + "status": "source_audit_complete_runtime_not_run", + "revisions": { + "native": "a4d46e2", + "python": "7827a6d", + "python_rpc": "0.50.0, matching uv.lock and installed package" + }, + "findings": [ + { + "finding": "Current generic and simple writable handlers have no finalize/finish override or explicit has_finalize; metadata inference is false. Native physical finalization closes/drains the current stream without a provider FINALIZE callback. Results are produced per INPUT batch.", + "sources": [ + "vgi/_test_fixtures/writable/generic.py:139", + "vgi/_test_fixtures/simple_writable.py:354", + "vgi/table_in_out_function.py:118", + "vgi/metadata.py:1450", + "native:src/storage/vgi_physical_write.cpp:271" + ] + }, + { + "finding": "Ordinary Python out.emit(response); raise does not send success then error. OutputCollector buffers; subprocess and HTTP flush only after process returns and output validates. A process exception takes the error path without flushing the collected success response; INPUT EOS has no user callback. Errors from an opted-in finalizer occur in FINALIZE init/read. This corrects the earlier proposed reproducer; serialization/transport failures are separate, unproven cases.", + "rpc_0_50_0_sources": [ + "vgi_rpc/rpc/_types.py:288", + "vgi_rpc/rpc/_server.py:1815", + "vgi_rpc/rpc/_server.py:1880", + "vgi_rpc/http/server/_app_stream.py:823" + ], + "native_sources": [ + "src/vgi_function_connection.cpp:1297", + "src/vgi_http_function_connection.cpp:1088" + ] + }, + { + "finding": "A custom table-in-out writer may opt into final output; worker finalization calls its callback and streams the returned batches. Native writable finalization only reads counts and does not collect deferred RETURNING rows; no explicit write-specific guarantee supporting that form was found. Current fixtures do not exercise this optional adapter gap. Qualify opted-in count finalization and zero-input behavior first. Use existing count/rows schemas if expanding support; do not add a finalization protocol or move native2 OLD/NEW work into the 1.5 baseline.", + "sources": [ + "vgi/table_in_out_function.py:238", + "vgi/worker.py:4824", + "native:src/storage/vgi_physical_write.cpp:232", + "native:src/storage/vgi_physical_write.cpp:289", + "native:src/storage/vgi_physical_write.cpp:588" + ] + } + ], + "existing_test_cases": [ + "native:test/sql/integration/simple_writable/returning_validation.test covers ordinary per-batch RETURNING/schema rejection, not writable FINALIZE.", + "native:test/sql/integration/table_in_out/exceptions.test and multi_batch_finalize.test cover generic finalize errors/multiple batches, not writable FINALIZE." + ], + "validation_limits": "EC2 SSH timed out on 2026-10-22. No local or remote tests/builds, runtime fixes, protocol changes or release-gate completion. Existing test cases were inspected, not rerun. Generic finalization SQL tests do not establish writable RETURNING support; simple SQLite writable fixture has no transaction support and cannot prove rollback.", + "next": "Transaction exception cleanup remains the first concrete fix. Reproduce the signed-HTTP token candidate separately. Treat deferred RETURNING as opt-in adapter work and require a reachable failure before changing the raw pre-FINALIZE drain." + }, + "write_finalization_runtime_checkpoint_2026_10_22": { + "date": "2026-10-22", + "status": "seven_expected_native15_subprocess_outcomes_verified", + "host": "ec2-user@18.234.138.19", + "remote_workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "artifacts": "logs/write-finalize-20261022-F5qbim/verified", + "runner_log": "logs/write-finalize-20261022-F5qbim/verified-runner.log", + "baseline": { + "native_synced_head": "03bd03221b43a81969d7d3af1c773e0b5b1c8a30", + "python_synced_head": "699c3fe5ae92138733eb02b01d19ea4f29a3e05f", + "native_build_source": "7588d45947ff4ba11b4dee1e5b53898d859996fa", + "python_runtime_source": "9642dae3e8151f1a160ced817f3741231d8b0bed", + "engine": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "engine_version": "Haybarn 1.5.5-rc1, DuckDB v1.5.6-dev149", + "python_version": "3.14.7", + "rpc_version": "0.50.0", + "pyarrow_version": "25.0.1", + "reuse_basis": "Both remote source trees were clean before fast-forwarding documentation commits; local runtime-source diff from the previous verified build/checkpoint is empty. Added only diagnostic runner/provider and documentation. No rebuild required.", + "cli_sha256": "93c9a06341698ade678f4463da4b35da59a05397067212810c07112d0e9b063d", + "extension_sha256": "df9af0daffc9a46b64d6306a3293acb2ca01dec7b9e306146d7301210e4d5fe4" + }, + "diagnostic_sources": { + "native:test/run_write_finalize_probe.py": "0ea02f75bf7cf423e3e0ecfd138eb5f117e2959d094d218f65ad17cfaa36fabb", + "tests/catalog/write_finalize_probe.py": "8396b18e8bb0b4057e8c84012c9b65d457b278d3d61d254c298b729369fea7d3" + }, + "results": [ + "input_returning: two exact rows, one INPUT batch, no finalize callback.", + "emit_then_raise: buffered_emit and input_raise traced; process error reaches the client and no successful rows are returned.", + "finalize_count: two rows accepted, one finalize callback with matching execution identity.", + "finalize_multiple_batches: 5000 rows across three INPUT batches, one finalize callback sees all 5000.", + "finalize_zero_input: no INPUT batch, one finalize callback sees zero rows and completes.", + "finalize_raise: one finalize callback; its error reaches the client with no successful rows returned.", + "deferred_returning: callback emits two id/label rows; native rejects them as incompatible count output, confirming the unsupported form." + ], + "fixture_lint": "python -m ruff check tests/catalog/write_finalize_probe.py passed on EC2; log at logs/write-finalize-20261022-F5qbim/ruff.log.", + "manifest_validation": "EC2 checker passed: 151 audit rows, 20 contract areas, 94 reviews remain. Log at logs/write-finalize-20261022-F5qbim/manifest.log; inventory coverage is not phase completion.", + "limits": [ + "Diagnostic sink only: no stored-table state or transactions, therefore no rollback, atomicity, transaction cleanup or production-provider proof.", + "Subprocess transport only; signed HTTP transaction-token candidate remains untested. No new runtime error-loss case established for raw pre-FINALIZE draining.", + "Count-mode checks prove successful callback/output processing and traced input totals; CLI does not print affected-row counts.", + "First harness attempt failed table materialization because it lacked a scan descriptor. Added a schema-only scan, then reran in a fresh verified artifact directory. The first attempt is not write-path evidence.", + "No production code or wire changes, no engine-pin change, no release-gate completion." + ], + "next": "Reproduce and fix transaction exception cleanup as its own checkpoint; then investigate the signed HTTP transaction-token candidate. Deferred RETURNING remains optional adapter work." + }, + "transaction_cleanup_checkpoint_2026_10_22": { + "status": "implemented_native15_verified_on_ec2", + "host": "ec2-user@18.234.138.19", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "artifacts": "logs/transaction-cleanup-20261022-5qEOLH", + "baseline_commits": { + "native": "6c72c379717e2143a004b119ce5052359d2aa2e2", + "python": "36892b1d2edff7065570e1f46b27d26af4929ef5", + "engine": "105edd31b57fe7e6d32ee648efad562e45a9f908" + }, + "source_sha256": { + "native:src/storage/vgi_transaction.cpp": "f3c251e691a527e0f9e49e473d78f6b9aa566253cdfba250c504ffa5a0be05a1", + "native:src/include/storage/vgi_transaction.hpp": "8536d31799ed0ab1de16d860b1e28b895e1eb945c59266240012305e70976fb4", + "native:test/cpp/test_transaction_cleanup.cpp": "9981c569e646e4ba2eae82127a53159b689b1625640ccae8ed25e057b1b029be", + "native:test/run_transaction_cleanup_probe.py": "543d391b523dd0f400c06556f1daa4d3083c1bedf6bb8c9ee02f5d46d2528446", + "tests/catalog/transaction_cleanup_probe.py": "3aae727dd571ab9e928d1ccda5f59e36a0daaddd121277e9c13d33f28f4da8c8" + }, + "binary_sha256": { + "vgi_catalog_tests": "d55d17b2faa328606c11fa9c0762931d8612c494e46209c805e82952dfe39c9d", + "haybarn": "40cec10e5ef4132deb6c02cf2182ff5a2f25a7fe5f6a279eefa8a51ab38d1428", + "vgi.duckdb_extension": "fbebb1c6677d8a0feed0a67a83a89614de1c4c1fd8b4c4f99af4e242632d1e70" + }, + "implementation": [ + "Take manager-owned transaction under its mutex; release the mutex before completion; local unique_ptr destruction releases the object on success or exception.", + "Commit retires transaction-local schema roots before provider dispatch, matching Rollback and preserving the existing bounded catalog entry retention policy.", + "No wire, engine pin, completion retry policy or provider error changes; no compensating rollback, savepoints or remote recovery machinery." + ], + "validation": [ + "Baseline: 960 assertions, 24 failures, all transaction lifetime checks in four provider-error scenarios; six provider trace validators passed. See before/stdout.log and before/results.json.", + "Fixed: all 960 native assertions pass; six scenarios, three cycles each, 42 observed transactions. See after/stdout.log and after/results.json.", + "Each commit/rollback success, reject-before-completion and error-after-completion case verifies object destruction, actual loaded schema-root retention until catalog teardown, fresh transactions, prepared-statement reuse and an unaffected second connection.", + "Provider traces prove one requested completion attempt per token, matching attachment ownership, no opposite/compensating operation, and one successful recovery rollback per tested cycle. Only deliberately rejected provider transactions remain active.", + "Existing run_semantic_alter.py --case ddl_transactions passes pending metadata visibility, rollback, commit and reconnect readback; see semantic-alter.log.", + "EC2 builds vgi_catalog_tests, shell and vgi_loadable_extension pass. Fixture and native runner Ruff checks pass. No local builds/tests." + ], + "review": "Independent read-only code review found no blocking issue in pinned1.5 lifetime, mutex scope, metadata retirement, original error propagation or regression assertions.", + "limits": [ + "Diagnostic provider has no user row mutations; no new row rollback or production-provider atomicity guarantee.", + "Application error after completion is not transport response loss. Existing duplicate-completion/idempotency behavior was not newly qualified.", + "Rejected remote transactions remain provider-owned; local cleanup does not determine or change their remote outcome.", + "BEGIN allocation/cleanup, signed HTTP token handling, deferred RETURNING and native2 adapter/trigger qualification remain separate.", + "Existing catalog retention bound is unchanged. No protocol publication, capability enablement or release gate closure." + ], + "next": "Duplicate completion, BEGIN retry/cleanup, signed HTTP token handling and bounded persisted INSERT/CTAS rollback are covered by the subsequent transaction_write_checkpoint_2026_10_22; continue broader qualification within the existing transaction lifecycle." + }, + "transaction_write_checkpoint_2026_10_22": { + "status": "implemented_bounded_native15_and_python_evidence_not_release_qualification", + "date": "2026-10-22", + "scope": "Complete transaction lifecycle checks and real stored-data write rollback while retaining the pinned 1.5 engine and existing transaction/write protocol.", + "ec2_root": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "engine_pin": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "engine_version": "Haybarn 1.5.5-rc1 / DuckDB v1.5.6-dev149", + "baseline_heads": { + "native": "eba0b96b50b82788ad6ede3986e95f63734c8f51", + "python": "4c1e309a51c3c1003b2453007d3f6a784686396e" + }, + "source_sha256": { + "native:src/include/vgi_attach_parameters.hpp": "0ee06bb33276176f0de20c3b5068eb5d6106da384b014706eff76f968aa73a3a", + "native:src/include/vgi_unary_retry.hpp": "4e6845bff6981b5f198db884227815ebaf38a01d4dbfba1c5342f368404a7a4e", + "native:src/include/vgi_unary_rpc.hpp": "426f4ac4d569914417ff7f131c3b9afe4fdc70db4b687fbbe4767b4647b7a3ef", + "native:src/storage/vgi_transaction.cpp": "78ec61db9fc713120605a7881642fee9715400620b6a501fd7830eed2461751b", + "native:src/vgi_catalog_api.cpp": "27e8b9a1fa52305c8536394b707292be7bbf035783db20a1f9efe2daf33e224c", + "native:src/vgi_http_client.cpp": "c44572ac0a4189466fac2241e25b03f8c2fbce9abe4c10deb45ac92681e5ceaa", + "native:src/vgi_unary_rpc.cpp": "ad1f0dbf1a1a31a17e159862931dc20a8fac933c7c2142cddced2ae417556662", + "native:test/run_transaction_begin_probe.py": "032ffa2b240167aec60b19ca645e626bd00615d4bb716404749a9bd00f23739e", + "native:test/run_transactional_write_probe.py": "6b92da25e321a5bc23154d7b46831ef905cd23287529c7296d73b4384162bfd7", + "vgi/table_function.py": "b3cf999dd446e9e57264be984c740aea078f5eced94ad4e9e609f3d8ee46aeda", + "vgi/worker.py": "950f4e7ebb9b88805b91293bb9ae3be40abb8d2983e218b46ddd3db32bc8c8f1", + "vgi/protocol.py": "c48fb098579869c432175232ce7696929caa2f7fd36ce42e3679677a6a5452c2", + "vgi/meta_worker.py": "74fc9ac98545c90cd9f5e41d5c5fdc0703663117a715e1c28348f1a47a638a04", + "vgi/_test_fixtures/writable/table.py": "2f11053dd0be4aa24e4b81ab6c90ba1834be22520cf79f28e14db29ec8918026", + "vgi/_test_fixtures/writable/generic.py": "d6cf7a0297062ef23481155d775b4023da775c2efd1887bafab70dbda81f27a1", + "tests/catalog/test_writable_signed_transactions.py": "9fc568c37b197ec8515a035448346cbdcf86885f250809a982d9db112159578d", + "tests/catalog/transaction_begin_probe.py": "05b7dd414dc656b1eb394f26e732a25a79a99b7873af8c97b6662630a1d36a2b", + "tests/catalog/transactional_write_reference.py": "9851ef375b95f2625d2ced2ba30b7d44794be0b0b640c2a27e6073a1c4575285", + "tests/transactor/test_completion_lifecycle.py": "c4c6f6b9c2c112ad0123637fda98f4e3ccd9240d3a7cd612247eaa6edf15dd3d" + }, + "artifacts_sha256": { + "build/release/haybarn": "303230f7c9671708f6590631a2afa16c65f0520e3a3b1e8e4a77032996a24f14", + "build/release/extension/vgi/vgi.duckdb_extension": "d00bd4e9ae5b4690a2193e45b693027c87b132ec88f9a3f3b50ba2ef479f8dc2" + }, + "changes": [ + "Classify catalog_transaction_begin as single-attempt. Preserve healthy subprocess pool reuse and stop classified shared-container retry after dispatch; allow container readiness retry before dispatch.", + "ATTACH primes ordinary and single-attempt concrete HTTP provider parameter objects. Single-attempt cache stays immutable, preserves provider credentials and avoids secret-manager/MetaTransaction reentry during BEGIN.", + "Authenticate transaction tokens at the Python callback boundary and expose provider bytes through BindParams/InitParams/ProcessParams, including HTTP producer/exchange rehydration and MetaWorker routing. Wire requests and continuation state remain sealed.", + "Writable fixtures use authenticated callback transaction bytes for schema discovery and processing.", + "Correct abandoned-BEGIN cleanup documentation: detached transactor has no TTL reaper, and client disconnect does not reclaim a transaction. Explicit completion or provider shutdown does.", + "No protocol/schema/version or engine-pin change, COMMIT/ROLLBACK retry change, savepoint, automatic compensation or recovery service." + ], + "validation": { + "completion_lifecycle": { + "artifacts": "logs/transactor-lifecycle-20261022-rrDFnq", + "result": "9 pytest items passed, including six new lifecycle cases plus registration/autouse checks; no skips. Real DuckDB DML and DDL, repeated same/opposite completion, unrelated active transaction, disconnect and shutdown persisted readback.", + "limit": "Stock Python DuckDB lacks custom begin/subcursor APIs. Tests directly register real ordinary DuckDB cursors with the unchanged transactor and exercise completion over Unix RPC." + }, + "begin": { + "baseline": "logs/transaction-begin-20261022-zb9zjk/baseline", + "pool_baseline": "logs/transaction-begin-pool-20261022-iPbqJ2/baseline", + "final": "logs/transaction-begin-pool-20261022-iPbqJ2/final", + "result": "Before guard: lost reply transparently succeeds after two allocations, leaving the first orphan. Final: an error, exactly one allocation, no compensating completion; three healthy autocommit queries reuse one worker PID with no orphans.", + "limit": "The one lost-response allocation remains provider-owned; no automatic orphan cleanup is claimed. Shared-container retry guard is compiled/source-reviewed, not runtime-qualified." + }, + "signed_http": { + "artifacts": "logs/signed-tx-nml2v7", + "result": "Before: four failures. Final: seven focused pytest items pass; 124 existing regressions and 74 callback/storage items pass. Count/rows commit/rollback, wrong-attachment rejection before effects, Worker/MetaWorker callbacks, multiple INPUT turns/finalize, sealed continuation state.", + "limit": "Real WritableWorker/GenericTableInsert signed HTTP with a real SQLite transaction adapter replacing only the unavailable custom transactor transport boundary." + }, + "persisted_native_writes": { + "artifacts": "logs/transactional-writes-20261022-2ke59i/verified", + "result": "17/17 passed: five success cases and twelve autocommit/explicit INSERT/CTAS init/second-input/finalize failures. Independent SQLite committed readback proves rollback of partially stored rows and created tables, including earlier successful writes even when COMMIT follows the error. Exact traces require one rollback and no commit for each failed transaction, and all begun transactions complete exactly once. Failed RETURNING yields no partial successful rows; recovery write succeeds.", + "success_cases": [ + "count_success", + "rows_success", + "explicit_rollback", + "zero_finalize", + "ctas_success" + ], + "failure_dimensions": { + "transactions": [ + "autocommit", + "explicit" + ], + "operations": [ + "insert", + "ctas" + ], + "phases": [ + "init", + "input", + "finalize" + ] + }, + "limit": "Single-process loopback signed HTTP SQLite reference with 5000-row multi-batch inputs; no production backend, concurrent writer, crash recovery or full custom DuckDB transactor qualification." + }, + "native_regression": { + "artifacts": "logs/transaction-final-20261022-KgRqSj", + "result": "Existing semantic ALTER ddl_transactions passes pending visibility, rollback, commit and reconnect. native-evidence.json records tested source/binary hashes." + }, + "quality": { + "artifacts": "logs/transactional-writes-20261022-2ke59i/quality", + "result": "Whole Python repo Ruff check passes; mypy passes 223 source files. Format check: 507 files already formatted; one existing formatting failure in unchanged tests/catalog/write_finalize_probe.py, present in baseline commit 36892b1. Changed production files and new fixtures pass focused formatting/lint." + }, + "manifest": { + "artifacts": "logs/transactional-writes-20261022-2ke59i/manifest.log", + "result": "Checker passed: 151 audit rows, 20 contract areas, 94 reviews remain. Inventory coverage is not phase completion." + }, + "full_launcher_integration": { + "artifacts": "logs/transactional-writes-20261022-2ke59i/integration.log", + "status": "not_green", + "test_cases": 367, + "reported_passed_including_skips": 301, + "actual_passed": 242, + "failed": 66, + "skipped": 59, + "baseline_constraint_failures": "63 CHECK-expression negotiation failures and one missing constraint read/enforcement profile. Sources are unchanged at both baseline heads; representative tests reproduce both groups with isolated Python 4c1e309 and the final native binary.", + "unresolved_full_run_failures": "cursor_pagination stream-header EOF and filter_pushdown nested_sequence count 0 versus 10. Both pass focused isolated baseline and current Python reruns; current rerun: 201 assertions, two cases, no skips. Full-run interaction/root cause remains unresolved; no full native baseline rebuild/rerun is claimed.", + "limit": "These branch-wide failures remain open; passing focused transaction tests do not make the full branch qualified.", + "classification_artifacts": "logs/integration-triage-20261022-4h4Gj1/{classification.json,python-baseline.log,current-focused.log}" + } + }, + "limits": [ + "EC2 Python DuckDB 1.5.5 does not provide enable_suspended_queries or subcursor(). No claim that the complete custom DuckDB transactor BEGIN/write path ran.", + "BEGIN response loss can still leave the original unknown token at the provider; this change prevents an additional allocation and does not determine its outcome.", + "Ordinary HTTP parameter timeout mutation is unchanged; immutability applies to the single-attempt cache.", + "No new protocol fields, matched release, broad phase completion or native2 runtime qualification." + ], + "next": "Implement persistent trigger CRUD/readback and the minimal native2 ENGINE adapter using existing transactions and write results. Qualify native2 OLD/NEW, expanded ALTER and multi-DML before enabling them; qualify BACKEND triggers and deferred RETURNING separately. Complete custom DuckDB transactor qualification when a compatible Python build is available." + }, + "regression_checkpoint_2026_10_23": { + "date": "2026-10-23", + "status": "bounded_native15_constraint_read_and_scan_framing_fixes", + "ec2_root": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "baseline_heads": { + "native": "eaaab1443599813487624ab425771838f12e3b83", + "python": "cf1a06ed6e60f93ce5bfc8b673863487bde6ddd2" + }, + "engine_pin": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "source_sha256": { + "native:src/include/vgi_arrow_ipc.hpp": "1e22a709c2667ee998831da6d7bf95da8ec2e48950788866edb261370263c077", + "native:src/include/vgi_constraint_contract.hpp": "8ded28bf85c786f8d112b7032c5de5c0e73d8199ce8c73a6d5c81011675cc4dc", + "native:src/storage/vgi_catalog.cpp": "616c46fe9f78bc71130074e783b9c410075737c8270e1713b7be09bc0826d859", + "native:src/storage/vgi_physical_write.cpp": "9065c08c1b9241675e535f0c6f90e7f231c671aebbad42bafdb92ea55212c7f2", + "native:src/storage/vgi_table_set.cpp": "ab56ee8d0df910a87098e0d259df4e76eeced86f8c11b92c235f4c98de6d1a90", + "native:src/vgi_arrow_ipc.cpp": "6374173938e8d11422af535d88b63fab54f688a879f65c7d32bc43ef87cba2a3", + "native:src/vgi_catalog_api.cpp": "5aac30c603954da50af185cfdf3660be02673087824a24b7ce3ab747f7dac34e", + "native:src/vgi_constraint_contract.cpp": "43b41c9bec97a5bb676436bfe97457f97831272cd83aa8cbdc553186bf36ac8b", + "native:src/vgi_function_connection.cpp": "35b9f5e1b2deef48b026205f39b1f57be3e61eafb903109b46f1d4232870c46e", + "native:src/vgi_index_contract.cpp": "d7001bbc96ff755edb61c46edcd81f54913fb7f87692098ebcc335820706db1f", + "native:test/cpp/test_constraint_contract.cpp": "e783778a7c850da96f4470950d9856d4879b1bf8a0a239e070fbb31b3a639773", + "native:test/cpp/test_index_contract.cpp": "61cc2352c9ed91d806ff692e6a10fc8f779f9e7d8c4b41f8bb1979e508b54662", + "native:test/sql/integration/table/constraints.test": "eee609abf1024a0046026b01cdd94b7404f16c429ce1434a5bd8466944d92bbb", + "native:test/run_scan_truncation_probe.py": "c82c6640275aacd8d585f6196bf07cf7027bb1bbd57fb992dfe81cad1f6d48ca", + "tests/scan_truncation_probe.py": "b7d0ef0853d06b9bba9c62fc6a2e4f0f8419febd44be988cb7c7ee50fd046e22" + }, + "artifacts_sha256": { + "build/release/haybarn": "8f5b5ca513f140e9b1e1ee326b045a5a95805b5ed350aa2b53880ef88d2984a8", + "build/release/extension/vgi/vgi.duckdb_extension": "dbab5b1d78c17b7c3a492f77b45d021ec04f5a7035b0a5ac05f4175a6d762d73" + }, + "constraint_read": { + "changes": [ + "Canonical trusted ENFORCED/ALL_ROWS facts require selected read authority, not write-enforcement authority. All four table materialization paths check selected read profiles, including time travel.", + "Offer the existing scalar expression profile in READ only and import bounded numeric CHECK comparisons after exact envelope, owner/context, AST, column and Boolean binding validation.", + "Explicit enforcement gates protect constrained INSERT/UPDATE/DELETE/MERGE planning, physical write resolution including CTAS, constraint DDL and unique-index DDL. Read-only discovery cannot grant writes.", + "Existing immutable Python fixture metadata and trust states are unchanged; no semantic metadata is removed or downgraded." + ], + "qualified_check_scope": "One =, <>, <, >, <= or >= comparison of primitive signed numeric columns (including FLOAT/DOUBLE) and numeric constants. Explicit default context and owning flat schema required; actual table binding must produce BOOLEAN. No function, cast, qualified/ambiguous/synthetic/generated/extension-typed operand, other expression shape/context, extra query clause or statement. CHECK creation remains rejected.", + "validation": [ + "16 C++ constraint/index cases, 415 assertions passed; positive numeric CHECK, malformed envelopes, unsupported semantics, read/write and unique-index authorization boundaries.", + "Four native SQL probes, 308 assertions and no skips: catalog enumeration, comments, constraints and time travel. Added SQL INSERT asserts native enforcement rejection before fallback discovery and unchanged row count." + ], + "artifacts": "logs/regressions-20261023-6SzJ8G/{build-constraints-fixed.log,constraints-unit.log,constraints-integration.log}" + }, + "scan_framing": { + "baseline": "Injected real launcher worker termination after one valid batch produced successful count0 after an empty filtered batch and count100 after a nonempty batch from an incomplete 10000-row producer. Split termination was accepted through missing EOS and then failed while closing input for the next split.", + "fix": "FunctionConnection raw FD input enables complete reads: physical EOF before the explicit Arrow EOS marker produces IOError rather than successful stream end or tolerated ArrowInvalid. Other FdInputStream callers retain their previous default.", + "validation": "Six cases each on launcher and subprocess (12 passed): early termination after empty/nonempty/split output rejects with no successful aggregate result; healthy 99-empty-batch, empty-result and paginated controls complete. Baseline six cases captured against copied pre-fix artifacts.", + "artifacts": "logs/scan-investigation-20261023-t8GYBQ/{truncated-baseline-final,fixed-launcher,fixed-subprocess,final.sha256}", + "limits": "The original spontaneous disconnect was not reproduced. Nine predecessor cases plus one spatial skip passed; 30 same-session cycles passed 90 result checks while mixing 30 expected catalog errors, 30 abandoned split scans, pagination and long empty-batch runs. Injected split truncation did not reproduce the original header-EOF text; the original disconnect trigger remains unproven." + }, + "dependency_followup": { + "artifacts": "logs/idle-race-audit-eREs07/{reproduce_idle_shutdown.py,result.json}", + "finding": "Independent vgi-rpc0.50.0 launcher idle-shutdown race reproduced with controlled timer-before-accept ordering, a real handler and the production10-second join. A new handler can be cut off by a shutdown request set while the worker was idle.", + "limit": "No production dependency change. The original full-suite log spans174 seconds, below the default300-second idle timeout, and has no evidence of an inherited expiring worker. This race is not attributed to the original scan failure." + }, + "limits": [ + "No wire/schema/version/engine-pin change or retry/recovery change.", + "Bounded CHECK discovery does not qualify general scalar SQL, CHECK creation, other trust states, nested constraints or native2 execution.", + "The original intermittent worker-disconnect trigger remains unknown; deterministic truncation coverage establishes the corrected client failure behavior." + ], + "next": "Superseded by combined_protocol_review_2026_10_23: complete included column/default-schema/macro migrations and optimizer P1 proofs. Trigger implementation deferred; track independent RPC idle race separately.", + "full_launcher_integration": { + "artifacts": "logs/regressions-20261023-6SzJ8G/integration-full.log", + "result": "308 executed cases passed, 11538 assertions, zero failures; 59 environment-dependent cases skipped.", + "executed_passed": 308, + "failed": 0, + "skipped": 59, + "assertions": 11538, + "configuration": "Pinned 1.5 release CLI/unittest, launcher subprocess transport, strict no-error-skip config and unique worker command to isolate other sessions.", + "scope": "All64 prior constraint failures and both previously failing scan tests pass. One full clean run does not identify the original spontaneous disconnect trigger or qualify skipped transports/backends." + }, + "quality": { + "artifacts": "logs/regressions-20261023-6SzJ8G/quality-and-source-hashes.json", + "result": "Whole-repo Ruff check passes; mypy passes223 source files. Format check:508 already formatted, one unchanged baseline failure tests/catalog/write_finalize_probe.py. Changed files pass focused lint/format. All15 native/Python source/test hashes match local copies and final binary hashes are recorded." + }, + "manifest_validation": { + "artifacts": "logs/regressions-20261023-6SzJ8G/manifest.log", + "result": "151 audit rows and20 contract areas covered;94 scope/capability/field reviews remain. No release gate closed." + } + }, + "combined_protocol_review_2026_10_23": { + "date": "2026-10-23", + "status": "source_scope_and_field_review_complete_inventory_validated", + "source": "docs/protocol/shared-catalog-values.md#combined-protocol-review--october-23-2026", + "basis": { + "native": "a4921dcb2929f2bb14e6c9c8049b5cf7cbe0944f", + "python": "65c6851fb87949bea36fb84244d2ca7bec751685", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "engine_20": "e1784fa4231072d7152b8aaa20977bdb8ff4ace3", + "engine_worktree_head_observed": "e211f3ff83d37cbec73e12dd53fe8edf923eaf4e", + "limits": "Use pinned git-show evidence or files unchanged at the pin; no silent engine refresh. No builds/tests/runtime edits." + }, + "coverage": { + "audit_rows": 151, + "areas": 20, + "original_audit_unchanged": true, + "previous_pending_review_entries": 94, + "scope_and_field_mappings_reviewed": true, + "limits": "Field disposition is not runtime readiness. Optimizer schemas/acceptance and listed P1 migrations are explicitly unfinished.", + "remaining_pending_review_entries": 0 + }, + "included_wire_work": [ + "Implemented: required SqlExpression IPC for catalog_table_column_default_set.expression and nullable SqlExpression IPC for catalog_table_column_type_change.expression, plus canonical CREATE/ADD default metadata. No new capability flag or second column RPC family.", + "Implemented nullable CatalogAttachResult.default_schema with None/main/literal-dotted corpus cases and SDK consumer changes. Empty/NUL present strings reject. Native1.5 rejects absence before attachment publication; no new capability or nested search-path encoding.", + "Implemented one required nonempty overload-family representation; single definitions become one-element families through local authoring helpers. Legacy parent wire fields are removed together with native1.5 family materialization and SDK migration. Function argument defaults remain unchanged; no new capability flag.", + "Retain projection-expression and repeatability design in the combined package; mark P0 scope reviewed and P1 contract/implementation proof outstanding. Reuse existing transport for metrics, partition selection and parallelism unless a concrete missing datum is demonstrated." + ], + "planned_column_profiles": [], + "column_contract_work": [ + "Canonical DEFAULT/USING, comments/tags, nullable default-schema, macro families and bounded virtual generated CREATE/read are implemented; finish runtime qualification and remaining provider/adapter acceptance.", + "Native1.5 generated columns use existing DuckDB execution over physical fields. Generated DML/ALTER ADD/stored/nested and unqualified scan forms reject; broader native2 port remains separate.", + "Enforce exactly one SELECT for view definition and qualify owner-relative late binding; no additional view wire carrier or fixed-output contract selected." + ], + "column_acceptance": [ + "DEFAULT/USING must work without a catalog-capability offer; malformed/unknown-profile/context/owner inputs reject before effects. Existing nested-operation, transaction and constraint requirements remain. Generated execution stays separately qualified.", + "Cross-SDK exact SqlExpression IPC/context/owner preservation for default and USING parameters, typed NULL default, explicit DROP, and malformed/oversized records.", + "Default CREATE/ADD/SET/READ consistency and per-omitted-row evaluation; virtual DAG/cycles/type/context rejection; USING old-schema row evaluation and failed-cast rollback.", + "Nullable default schema versus main versus raw dotted name across all SDKs, native1.5 fail-closed handling, native2 optional default semantics.", + "Strict column tags map rejection (duplicates/non-string values/malformed UTF-8/JSON), empty/absent comments/tags, unrelated metadata preservation, CREATE/ADD readback.", + "Views reject extra SELECT/script statements, preserve owner binding under changed search_path/defaults, derived names/types/aliases across rebind and prepared queries, barrier rejection on unsupported native readers." + ], + "additional_field_deferrals": { + "remote_compression": { + "reason": "DuckDB physical codec enum has no established provider-independent remote semantics; native already rejects non-default compression.", + "later_release_cost": "Existing typed options or Arrow metadata plus a new profile can accommodate a demonstrated backend requirement; no new RPC intrinsically required." + }, + "sql_change_ownership": { + "reason": "No principal ownership/authorization transfer contract; dependency owned is lifetime ownership only.", + "later_release_cost": "A future ownership transfer needs an explicit authority model and likely new profile/operation; accept that later coordinated-release cost instead of inventing an unneeded security model." + }, + "stored_generated_columns": { + "reason": "Carrier can preserve stored category, but there is no qualified physical rewrite/execution profile.", + "later_release_cost": "Existing canonical expression/category fields suffice; future qualified profile can enable semantics without another carrier." + }, + "triggers_and_new_transaction_framework": { + "reason": "Combined review recommends deferring new trigger CRUD/execution; the user has explicitly excluded unnecessary transaction/recovery/savepoint frameworks. Existing transaction/write phases remain authoritative.", + "later_release_cost": "No speculative trigger requirement or generic statement/replay/savepoint fields in this review; any future proven wire gap is separately coordinated." + }, + "fixed_output_views": { + "reason": "Current contract stores a SELECT definition/aliases and derives outputs by owning-engine binding.", + "later_release_cost": "A future provider requiring immutable persisted output schema would need an explicit negotiated schema/validation contract; this release does not promise it." + } + }, + "optional_area_decisions": { + "sequences_types": { + "decision": "defer", + "reason": "Neither is required merely to port the 1.5 VGI behavior to 2.0. Sequence semantics require allocation authority and a genuine engine extension point, not discovery fields. Named-type identity/ENUM domain is a real schema-fidelity feature, but resolved Arrow storage is the currently supported interoperability boundary; the 2.0 constructor callback addition is not transferable metadata.", + "business_cost": "Clients cannot use a remote catalog sequence as normal nextval/currval or allocate keys uniformly across clients. Schema migration/ORM tooling cannot discover/recreate named aliases/ENUMs, and CAST to a remote named type is unavailable. Ordinary supported primitive/nested Arrow value exchange continues; this must not be described as named-type or ENUM semantic coverage.", + "later_release_cost": "Sequence list/get/create/drop/allocate and type list/get/create/drop would normally be additive methods, but require another coordinated SDK/client/worker update under exact version matching. Type identity/domain annotations must be negotiated; redefining existing unannotated dictionary/string semantics would be breaking and should be avoided.", + "reopening_proof": [ + "Sequence: real custom-catalog nextval adapter against pinned engine; two concurrent clients produce no duplicates; explicit rollback/gap policy, overflow/cycle/restart behavior, and ambiguous allocation failure without retry.", + "Types: alias/ENUM create/get/list/drop/reconnect on durable provider; preserve exact identifier, enum label order and full domain including labels absent from batches; reject invalid values; round trip table columns, CAST and prepared results through all SDKs; prove no silent alias-to-string semantic claim.", + "Resolve dependencies, replacement and rename behavior before freezing a named-type carrier; demonstrate nested nullable fields and unknown-profile rejection on 1.5." + ] + }, + "collations_coordinates": { + "decision": "defer", + "reason": "A collation is executable equality/order behavior, not a name plus flags. CRS descriptions are portable and comparatively simple, but new catalog CRUD is not required to preserve an existing spatial Arrow field's metadata. No demonstrated catalog-level consumer presently justifies another object family in this release.", + "business_cost": "Remote custom collations cannot become local SQL comparators. Remote CRS definitions do not appear as catalog entries in duckdb_coordinate_systems and cannot be resolved/created through a VGI CRS catalog API. Existing metadata preservation must not be advertised as certified transformation or custom collation support.", + "later_release_cost": "Catalog discovery/CRUD adds methods and coordinate-system object identity kind (currently absent from CatalogObjectRef), so it costs another coordinated version/SDK pass. Certifying a collation already represented by QueryEvaluationContext may be a named-profile implementation extension without changing fields; executable remote collation callbacks need a separate qualified execution design.", + "reopening_proof": [ + "Collation: local/remote equality, ordering, NULL/Unicode behavior and supported composition agree at versioned engine/library pins; tests prove optimizer's binary-equality shortcut is sound.", + "CRS: durable authorized list/get and identifier round trip; specify conflicting authority/code versus definitions and both WKT/PROJJSON forms; prove catalog resolver behavior separately from display-only metadata; reject unknown semantic profiles.", + "Do not add empty descriptor placeholders or claim spatial operations from a metadata echo test." + ] + }, + "windows": { + "decision": "defer", + "reason": "Current aggregate RPCs already transmit partition data, masks, frame ranges and batched result positions. The 2.0 standalone WindowFunction abstraction adds peer/valid bounds, argument ordering, global/local sink/finalize state and delayed streaming. Reusing the aggregate name would conceal missing semantics; matching the new C++ callback layout would leak implementation details.", + "business_cost": "Existing worker aggregates can still be used as qualified windows. Authors cannot register arbitrary standalone ranking/navigation or custom streaming windows through VGI; DuckDB's own built-ins remain available locally.", + "later_release_cost": "Potential new function kind, advertised features and lifecycle records require coordinated SDK updates. A later bounded adapter that can prove exact equivalence with existing aggregate-window messages may avoid a wire change for that subset; do not promise this for general peer/offset/navigation functions.", + "reopening_proof": [ + "Choose a real function unavailable through aggregate-window callbacks and demonstrate pinned-2.0 execution with peer groups, NULL handling, exclusion and argument ordering.", + "Specify portable row/partition identity, bounds, memory ownership, cancellation/destruction, and streaming delayed output; test partition skew and multiple threads.", + "Keep unsupported options rejected and cross-SDK result/cleanup tests before freezing any added fields." + ] + }, + "statements": { + "decision": "defer", + "reason": "SqlStatement is a representation only; neither it nor a TABLE result grants mutation rights. The current query agreement deliberately selects only explicit/automatic read modes. Generic SQL sessions add result kinds, side effects, transaction-control and pooling rules that typed catalog DDL and existing write RPCs intentionally avoid.", + "business_cost": "Users cannot send arbitrary backend SQL writes/DDL or use CONNECT as a full remote SQL session. They retain explicit read queries, catalog DDL and supported table writes. Backend-specific operations absent from those typed APIs require provider-specific functions, not a false claim that read-query execution supports statements.", + "later_release_cost": "A future single-statement prepare/execute entry point and result-kind capability would be additive but still force a coordinated SDK/version update. Reusing current read-only preparation with mutating semantics would be breaking and unsafe. SqlStatement, typed parameter batches and existing transaction/result streams can be reused, so a new statement coordinator or recovery service is unnecessary.", + "reopening_proof": [ + "Start with explicit one-statement provider-dialect DML/DDL only; forbid scripts, ATTACH/DETACH, transaction-control and session mutations until explicitly modeled.", + "Prove zero/count/row result modes, finite and streaming results, rollback on body/finalize failure, cancellation, ambiguous-dispatch no retry, and no read-result cache reuse for mutations.", + "For raw CONNECT separately prove session isolation/pooling, credentials, disconnect/rollback and engine routing on 2.0; do not infer from EXECUTE_QUERY_NODE." + ] + }, + "multi_input": { + "decision": "defer", + "reason": "Pinned engine still rejects more than one TABLE subquery. The existing proposal proves only the tagged-union workaround, not N-child scheduling. An input_id field cannot by itself specify which input is ready/finished, buffering, cancellation or HTTP continuation ownership.", + "business_cost": "Multi-relation ML, bioinformatics, graph and spatial operators lose separate-input streaming and composability ergonomics. Passing table names to private connections can lose caller transaction visibility; recommend the already-demonstrated same-query tagged-input workaround where full buffering is acceptable instead of presenting private connections as equivalent.", + "later_release_cost": "New per-input schema/identity and completion/lifecycle transport is likely additive as a separate function profile/method, but entails a full coordinated SDK release. Replacing the existing single-input envelope would be major. Waiting for scheduling evidence avoids freezing a misleading universal input-ID field and later changing it again.", + "reopening_proof": [ + "Two heterogeneous bound TABLE subqueries delivered to one operator in caller transaction, including uncommitted data and one empty input.", + "Small-build/large-stream example demonstrates bounded buffering, input completion and no deadlock; verify cancellation and errors on either input.", + "Prove subprocess and HTTP continuation behavior, transaction propagation, ordering guarantees and input schema drift before freezing portable messages." + ] + }, + "triggers": { + "decision": "defer", + "reason": "The additional contract is justified specifically by VGI-managed persistent cross-client trigger definitions, not by backend triggers already enforced internally or by the existence of 2.0 table hooks. That product requirement is not established. Freezing metadata before proving exclusive execution ownership, exact OLD/NEW and statement behavior would advertise semantics that the current native path does not provide.", + "business_cost": "No portable CREATE/list/get/drop/metadata API for remote trigger definitions, no engine-executed VGI triggers, and no cross-client trigger introspection. A backend may continue owning internal triggers as part of its actual write implementation, but per-batch SQL does not establish original SQL statement-trigger semantics and is not a new advertised VGI capability.", + "later_release_cost": "Trigger CRUD is approximately five new methods plus definition/info/create records and selected capabilities: additive in shape but a coordinated SDK/version redistribution under current matching. Existing CatalogObjectRef(kind=trigger,owner_table), SqlStatement, dependencies and OLD/NEW changes significantly reduce future change size. This review does not justify new general WriteRequirements, statement IDs, tickets, savepoints or recovery RPCs.", + "reopening_proof": [ + "A real user/provider need for persistent cross-client trigger management, then one durable CRUD/reference lifecycle with complete authorized snapshots and dependency closure.", + "Pinned-2.0 native ENGINE mode proof for a deliberately bounded statement trigger: order, zero input, multiple batches, exact rows/changes mapping and full transaction rollback after action/finalization failure.", + "Exclusive ENGINE versus BACKEND ownership, incapable writer rejection and current definition checks through existing bind/transaction mechanisms; do not infer a new wire field from hypothetical stale prepared plans.", + "Keep pinned unsupported grammar rejected. BACKEND definitions cannot be installed as executable native entries without addressing ScanTriggers conflating discovery/execution." + ] + } + }, + "rollout": "Selected: separate versioned endpoints/processes, draining old sessions/transactions, then reattach/reprepare. Retain exact major/minor matching. Do not migrate opaque tokens/transactions or automatically retry ambiguous mutations. Rollback needs old-compatible catalog state or a tested restore/migration. P3 deployment proof remains; no version-aware router selected.", + "validation": { + "status": "remote_inventory_and_focused_tests_passed", + "host": "ec2-user@98.81.121.120", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs": "logs/protocol-review-20261023-SBCl67", + "inventory": { + "audit_rows": 151, + "contract_areas": 20, + "pending_reviews": 0, + "require_decisions": "passed" + }, + "manifest_tests_passed": 11, + "commands": [ + "python scripts/check_duckdb2_manifest.py --native-root ../vgi --require-decisions", + "python -m pytest -q tests/test_duckdb2_manifest.py" + ], + "limits": "Inventory/consistency validation only. No runtime code, wire/version/engine-pin change or native2 qualification. The prior 308-case native run is not counted as validation of this documentation review." + }, + "gate_effect": { + "P0": "Complete: all source mappings reviewed; remote inventory --require-decisions and 11 focused tests passed.", + "P1": "Open: optimizer contract/reference proofs and remaining provider/adapter acceptance. DEFAULT/USING, nullable default-schema, macro families, descriptive metadata and bounded virtual generated definitions use mandatory representations without new capability flags.", + "P2_P3": "Open: complete generated diff, all-SDK conformance, selected version/routing, capability matrix and deployment/rollback exercise.", + "M0_M4": "Open: native2 port and qualification, separate from this protocol review." + }, + "withdrawn_column_profile_proposals": [ + { + "name": "vgi.column.default.v1", + "status": "withdrawn_by_user_2026_10_23", + "reason": "Mandatory unpublished wire migration; no new opt-in. Default metadata keeps its existing key independently of capability names." + }, + { + "name": "vgi.column.type.using.v1", + "status": "withdrawn_by_user_2026_10_23", + "reason": "Mandatory unpublished wire migration; no new opt-in. Default metadata keeps its existing key independently of capability names." + }, + { + "name": "vgi.column.generated.virtual.v1", + "status": "withdrawn_by_approved_implementation_2026_10_23", + "reason": "Canonical generated representation is mandatory in the unpublished successor. Provider operation support is validated before effects; no new representation opt-in.", + "checkpoint": "virtual_generated_columns_checkpoint_2026_10_23" + } + ], + "withdrawn_column_metadata_profile": { + "name": "vgi.column.metadata.create.v1", + "reason": "Existing metadata semantics are mandatory in the unpublished successor; no additional representation opt-in.", + "checkpoint": "column_metadata_checkpoint_2026_10_23" + } + }, + "column_expression_checkpoint_2026_10_23": { + "date": "2026-10-23", + "status": "implemented_bounded_validation_passed_one_preexisting_native_regression_open", + "base_revisions": { + "native": "8a6c4341311d6d3cc5425a96be24449024e3ca9e", + "python": "dc8a5528f96c4a86a47d4006f617591e9ae06617" + }, + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "contract": { + "set_default": "required binary canonical SqlExpression IPC", + "type_using": "nullable binary canonical SqlExpression IPC; absence is ordinary cast", + "create_add": "canonical base64 IPC field metadata using the same record", + "capabilities": "no new flags or default/USING scalar READ gate; existing unrelated requirements remain", + "public_python": "vgi.catalog.SqlExpression is the canonical record, not a string NewType" + }, + "semantics": [ + "Explicit SQL NULL remains distinct from absent USING/no default and DROP DEFAULT.", + "Exact record/context/owner validation precedes provider hooks; TYPE-only schemas reject expression metadata recursively.", + "Durable reference readback, rollback/commit/reconnect and failed-cast schema/data/version atomicity.", + "Native 1.5 default binding preserves authoritative metadata while using typed constants for supported defaults.", + "Signed HTTP/CLI exercise the real public path without requiring a catalog-capability offer." + ], + "limits": "Bounded deterministic primitive scalar grammar/default evaluation context on native 1.5 and durable reference; no generated/volatile/general SQL or native2 qualification. Static descriptor legacy discovery remains; custom DuckDB transactor rejects canonical expressions because it cannot preserve context.", + "validation": { + "host": "ec2-user@98.81.121.120", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs": "logs/column-expressions-20261023-JgPvmj", + "python_regression": { + "passed": 1439, + "skipped": 8 + }, + "python_focused": { + "passed": 101 + }, + "python_nested_type": { + "passed": 64 + }, + "ruff": "passed", + "mypy": "223 source files passed", + "generation": "--check --require-all passed", + "native_unit": { + "cases": 4, + "assertions": 50 + }, + "native_sql": { + "cases": 3, + "assertions": 251, + "scope": "column lifecycle/default/USING, required filter paths and view aliases" + }, + "sdks": { + "go": "62 corpus cases passed via real HTTP codec", + "rust": "3 column contract tests passed", + "typescript": "3 Arrow and 3 Flechette tests; 97 assertions per implementation; type build passed", + "java": "16 catalog contract tests passed", + "csharp": "23 catalog contract tests and solution build passed", + "cpp": "normal SDK/RPC build and corpus test: 322 assertions passed" + }, + "browser_client_sha256": "bab319c266b96faf3b81e1f4b9d4379882c10dca2a26dc19d4d82c0df0feced8", + "python_final": { + "passed": 85, + "includes": "nested/future-version metadata, revision-checked NOT NULL fixture regression, signed HTTP/CLI and 11 manifest checks" + }, + "manifest": "151 audit rows / 20 areas / zero unresolved scope mappings; --require-decisions passed", + "artifacts_sha256": { + "extension": "bdf76ad13b6b11b7f120d906a00ea2e3a3edaaaf516089275b3540affdacfe68", + "haybarn": "bb436a6965e7b0d3886d1fa0ab34299e76bfd7dab93628b65c9bcdf1dc9642cf", + "unittest": "20c0bd0c07ea6ad09dc94de79d7b15cd8b4bf9710f3aa2974167bd48e0e01820" + }, + "native_launcher": { + "reported_cases": { + "passed": 384, + "failed": 3, + "skip_markers": 37 + }, + "reported_assertions": { + "passed": 12459, + "failed": 3, + "skip_markers": 37 + }, + "corrected_fixture_recheck": { + "cases": 2, + "assertions": 8, + "log": "native-regression-fixtures.log", + "changes": [ + "Expected reflection hash updated for new payload fields.", + "Hostile row-ID fixture migrated from removed index arrays to canonical exact-path constraints; invalid storage identities reject explicitly." + ] + }, + "remaining_failure": { + "test": "test/sql/integration/schema_reconcile.test:40", + "error": "native 1.5 does not support nested constraint column paths", + "classification": "preexisting_source_proven_fixture_adapter_qualification_mismatch", + "evidence": [ + "Unchanged Python vgi/_test_fixtures/schema_reconcile/worker.py:81 declares nested.a nonnullable; lines 543-547 publish its enforced ALL_ROWS NOT_NULL fact.", + "Unchanged native src/vgi_constraint_contract.cpp:115-121 rejects nested paths while decoding at line 342, before write conversion.", + "Python fact predates this task at 79ba6a5; native restriction is present in pre-task 8a6c434." + ], + "action": "Preserve Arrow nullability/fidelity assertions. Resolve nested constraint/provider qualification separately; this is not a passing full regression suite." + }, + "limits": "Private-transactor writable lane and subprocess-only worker-pool case excluded by test_launcher; optional fixture skips remain. Initial wrong-fixture-path run discarded; final run uses unique commands against task sources." + }, + "source_integrity": "Changed files checked local versus EC2 by SHA256; source-checkpoint.json and commits.json in log directory identify exact sources and synchronized branch heads." + }, + "gate_effect": "Closes only the canonical DEFAULT/USING payload migration. P1-P3 and M0-M4 remain open; no freeze, public version change, engine update or native2 claim.", + "sdk_revisions": { + "vgi-go": "724588ead2c5f674408c5a561466ceba0b990067", + "vgi-rust": "a8a024a8fff2869225050d95f14d57f3f43d75d2", + "vgi-typescript": "c38bb024a560f3ce03bc2aaa722537212ff486d2", + "vgi-java": "ab6a22f18555e5d9275ba421943bf85266de1c6a", + "vgi-csharp": "56866268c5b73b86277eb27065ffcc84b3d77bf0", + "vgi-cpp": "a671498abc73e653f1bf58aaaa794fe8b3460613" + } + }, + "nullable_default_schema_checkpoint_2026_10_23": { + "date": "2026-10-23", + "status": "implemented_and_validated_on_15_with_explicit_absence_rejection", + "base_revisions": { + "native": "4ece92d2bc709f6ee1c9dc0774b90342414989f8", + "python": "73bd21c22716ca33eb066952c2b078c3de39ac10" + }, + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "contract": { + "field": "CatalogAttachResult.default_schema: nullable UTF-8", + "absent": "NULL means no implicit default schema", + "present": "one nonempty raw root identifier without NUL; literal dots never split", + "constructor_omission": "Existing provider defaults may remain main; explicit NULL never selects a fallback", + "capabilities": "No new negotiation or permission flags", + "function_registration": "Catalogs without a default register functions under explicit schemas" + }, + "implementation": [ + "Python attach result/declarative Catalog/provider response validation, signed HTTP discovery/load and CLI JSON preserve explicit absence.", + "All six worker SDK schemas/codecs and affected clients/provider defaults retain NULL, main and literal dotted names.", + "TypeScript generated builders distinguish undefined from explicit null when applying a non-null constructor default.", + "Native1.5 rejects absent/empty/NUL defaults before settings parsing and attachment publication; valid raw names retain behavior." + ], + "validation": { + "host": "ec2-user@98.81.121.120", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs": "logs/nullable-default-schema-20261023-3AVBK6", + "status": "passed", + "python_regression": { + "passed": 1454, + "skipped": 8 + }, + "ruff": "passed", + "mypy": "223 source files passed", + "generation": "--check --require-all passed", + "manifest": { + "audit_rows": 151, + "areas": 20, + "unresolved_reviews": 0, + "tests_passed": 11 + }, + "native_unit": { + "cases": 2, + "assertions": 41 + }, + "sdks": { + "go": "65 HTTP codec corpus subcases passed", + "rust": "1 catalog codec test + 1 public accessor test passed; cargo check -p vgi-client --tests passed", + "typescript": "2 Arrow + 2 Flechette attach/default tests and typecheck passed", + "java": "17 CatalogContracts + 23 HostedCatalog tests passed", + "csharp": "29 tests and solution build passed", + "cpp": "normal SDK target built; 1 test / 331 assertions passed" + }, + "browser_client_sha256": "e5983b73d2cc45b561f213049490fa300fc2bcf64bd96bddebd3e681aedb441c", + "protocol_hash": "457a2fc7c24a637480d2c34237ea0ccf826b10f4ea5e316d4623895f4a535585", + "protocol_hash_limits": "The existing fingerprint is unchanged for this nullability-only field change. Direct generated-schema and codec checks, not fingerprint equality, validate nullability.", + "native_build": "production/loadable/unittest/haybarn/catalog-tests targets passed", + "native_sql": { + "cases": 1, + "assertions": 16, + "scope": "absence rejects before publication/settings; rejected alias reused; main and literal dotted default work" + }, + "native_attach_regression": { + "cases": 12, + "assertions": 394, + "skipped": 5, + "scope": "existing attach/global functions/reflection", + "skips": "Optional HTTP versioning workers not configured" + }, + "source_integrity": "All changed files matched local/EC2 SHA256 before commit; source-checkpoint.json, commits.json and artifacts.sha256 are in the checkpoint log directory." + }, + "limits": "No native1.5 no-default execution claim. Native2 optional-return mapping/SQL behavior, protocol freeze/public major transition and broader release gates remain open. The previous nested NOT NULL schema-reconciliation regression is unchanged and outside this field migration.", + "sdk_revisions": { + "vgi-go": "6273e4abbe06209972237769d2e033ea50920cdc", + "vgi-rust": "e0fe7c71d748142a2f342bcc023b1fb21abb35d5", + "vgi-typescript": "0e713aa68d87d9e76670eab52c31337fbf2367d4", + "vgi-java": "bae44574113fa7670c98da69504fb7a58e4535aa", + "vgi-csharp": "02d2cec4517d483c16fbdbb2369438a2a8af887b", + "vgi-cpp": "48c41588768efc9a23371027ad35f65b74894557" + } + }, + "macro_family_checkpoint_2026_10_23": { + "date": "2026-10-23", + "status": "implemented_and_validated_on_15", + "base_revisions": { + "native": "2e7c6ac121d5a688d48a2c777633be5a86f8974c", + "python": "48700b763c9903058ade0a514661ad1bb6563c74" + }, + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "contract": { + "representation": "Implemented one required nonempty overload-family representation; single definitions become one-element families through local authoring helpers. Legacy parent wire fields are removed together with native1.5 family materialization and SDK migration. Function argument defaults remain unchanged; no new capability flag.", + "defaults": "CREATE accepts typed literals or expression defaults; owner resolves expressions once. Discovery accepts resolved literals only, preserving typed NULL separately from no default.", + "type_identity": "Declared Arrow types remain distinct from literal/default/documentation types.", + "context": "Every member has explicit matching owner and evaluation context.", + "publication": "Owner CREATE replaces the complete family atomically. Native installation validates wire/parse/type/context for all members before installing the full vector; body binding still happens at invocation and unresolved references can fail then. No skipped member or first-overload fallback." + }, + "implementation": [ + "Python client/provider/worker, declarative authoring conversion and complete-family read validation.", + "Six SDK wire schemas and corpus vectors for single/multiple members; provider helpers normalize singles.", + "Native1.5 strict family decoder, typed Arrow default import, scalar/table family installation and per-parameter introspection.", + "vgi_function_arguments adds zero-based overload_index for macros, explicit declaration types, literal-default displays and missing-versus-empty parameter documentation; macro arguments can be positional or named." + ], + "validation": { + "status": "passed", + "host": "ec2-user@98.81.121.120", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs": "logs/macro-families-20261023-Rulov6", + "python_targeted": { + "passed": 352 + }, + "python_regression": { + "passed": 1697, + "skipped": 8, + "generation_failures": 9, + "generation_recheck_passed": 9, + "limits": "The initial nine failures were stale remote generated SDK files; all nine parity cases passed after synchronization." + }, + "python_final_durable": { + "passed": 9, + "scope": "Signed HTTP creation/get/list/bulk/reconnect, atomic replacement failure, rollback/commit, scalar/table resolution, literal type mismatch, typed NULL/type metadata/empty docs." + }, + "ruff": "passed", + "mypy": "223 source files passed", + "manifest": { + "audit_rows": 151, + "areas": 20, + "unresolved_reviews": 0, + "tests_passed": 11 + }, + "generation": "--check --require-all passed; all 17 generated artifacts/corpora current", + "native_build": "production/loadable/unittest/haybarn/catalog-tests targets passed", + "native_macro_contract": { + "cases": 4, + "assertions": 101, + "scope": "Strict list/bulk IPC envelopes, malformed family/default records, Arrow typed defaults, scalar/table overload execution, named calls, documentation, cached context rejection, all-member parse failure and explicit invocation-time binding limit.", + "log": "native-macros-final.log" + }, + "native_metadata_regression": { + "cases": 3, + "assertions": 131, + "log": "native-metadata-configured.log" + }, + "native_sql_regression": { + "initial": { + "passed": 12, + "failed": 1, + "skipped": 13 + }, + "corrected_introspection": { + "passed": 2, + "log": "native-introspection-final.log", + "change": "Function-only named-versus-positional invariant retained; macros explicitly support both calling forms and overload_index." + }, + "configured_attach": { + "passed": 9, + "skipped": 5, + "log": "native-attach.log", + "skips": "Only optional HTTP versioning workers are absent." + }, + "limits": "Runs cover existing macro execution, function/macro introspection, catalog bulk/fallback/revalidation, global functions and attachments. Counts overlap across reruns. This is not a claim that the previously recorded full native suite regression is fixed." + }, + "native_fixture_lint": "Ruff and format check passed", + "sdks": { + "go": "628 tests/subtests passed, including real HTTP codec and hosted reflection", + "rust": "35 tests passed; example worker compile passed", + "typescript": "24 tests / 326 assertions per Arrow backend, type build and browser bundle passed", + "java": "133 focused tests passed", + "csharp": "59 focused tests passed", + "cpp": "397 assertions across 5 provider/codec tests passed; normal SDK build passed" + }, + "sdk_logs": "sdk/", + "browser_client_sha256": "840df926c9b2ebbe63d6c480eed2444ec984f34d29a33f8572d92ca9d4b55479", + "protocol_hash": "457a2fc7c24a637480d2c34237ea0ccf826b10f4ea5e316d4623895f4a535585", + "protocol_hash_limits": "The outer RPC fingerprint is unchanged by embedded MacroInfo/MacroCreateRequest layout changes. Exact generated schemas and corpus/real provider tests verify the breaking family migration; fingerprint equality is not compatibility.", + "source_integrity": "Changed files checked local versus EC2 by SHA256; source-checkpoint.json, commits.json and artifacts.sha256 in the checkpoint log directory identify exact sources, synchronized branch heads and binaries." + }, + "limits": "Native SQL CREATE MACRO remains unsupported. Broader SQL contexts/body forms, general dependency extraction, native2 engine port, P1-P3 freeze/release gates and the prior nested NOT NULL schema-reconciliation regression remain open. Native body semantic validity is not proven at load time; body binding occurs when called. Existing family comment empty-versus-NULL behavior is unchanged.", + "gate_effect": "Closes canonical macro-family wire migration and bounded native1.5 consumer checkpoint only. General body/dependency/provider qualification, remaining column/optimizer work, P1-P3 freeze/release and M0-M4 native2 remain open.", + "sdk_revisions": { + "vgi-go": "490e43be573288ddc4887d21e0a05860cc58c087", + "vgi-rust": "2cf4cac2b37352f508f689b316029420c6b964e5", + "vgi-typescript": "4eaafa84f6a49de5f0b6b681c091db041c8596e6", + "vgi-java": "3b30cb560cbc283eb6c1c9d858cded1c9cf3079d", + "vgi-csharp": "07057799d86274ddf5767e98aec1c190eeadeb24", + "vgi-cpp": "d3a5be374a75395a117f026b2ded3f1a67bb06c3" + } + }, + "column_metadata_checkpoint_2026_10_23": { + "date": "2026-10-23", + "status": "implemented_bounded_validation_passed", + "base_revisions": { + "native": "5b9d26f814c42e7c8706ab0c3e344570929a6580", + "python": "7dc36111c8552e07ee418719ac369b8276d0a686" + }, + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "contract": { + "keys": { + "comment": "Literal UTF-8 string; absent differs from present empty.", + "tags": "UTF-8 JSON object with unique decoded keys and string values; absent differs from present {}." + }, + "validation": "Reject malformed UTF-8/JSON, unpaired surrogates, non-string tag values, duplicate decoded JSON keys and existing invalid duplicate Arrow metadata keys. Recurse container/dictionary/extension fields before provider effects or discovery publication.", + "preservation": "Literal Unicode, NUL, dots, case and unrelated valid metadata remain unchanged; validation does not rewrite accepted payloads.", + "wire": "No record/RPC/version change and no new capability flag; existing comment mutations stay separate." + }, + "validation": { + "status": "bounded_runtime_and_conformance_passed", + "host": "ec2-user@98.81.121.120", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs": "logs/column-metadata-20261023-6KZ8Vg", + "corpus": { + "valid_schemas": 4, + "invalid_schemas": 27 + }, + "python_focused": { + "passed": 59 + }, + "python_regression": { + "passed": 1484, + "skipped": 8, + "initial_failures": 1, + "correction": "Root-owned unused-loop Ruff failure corrected; focused rerun passed." + }, + "python_final_corpus": { + "passed": 14, + "scope": "Includes added overlong, surrogate-encoded and truncated UTF-8 cases." + }, + "ruff": "passed", + "mypy": "224 source files passed", + "native_build": "production/loadable/unittest/haybarn/catalog-tests targets passed", + "native_metadata": { + "cases": 5, + "assertions": 183, + "log": "native-metadata-verified.log", + "scope": "CREATE/ADD preflight, both discovery paths, native comments/COPY, public persisted reconnect readback and 31 shared valid/invalid schemas." + }, + "native_regression": { + "unit_cases": 7, + "unit_assertions": 181, + "column_sql_cases": 3, + "column_sql_assertions": 251, + "catalog_sql_files_passed": 11, + "logs": [ + "native-metadata-regression.log", + "native-column-regression.log", + "native-catalog-regression.log" + ] + }, + "native_fixture_lint": "Ruff and format checks passed", + "protocol_hash": "457a2fc7c24a637480d2c34237ea0ccf826b10f4ea5e316d4623895f4a535585", + "protocol_hash_limits": "No RPC or record schema change in this checkpoint. Metadata meanings are validated inside existing Arrow schema carriers; the fingerprint alone does not prove semantic compatibility.", + "browser_bundle_sha256": "840df926c9b2ebbe63d6c480eed2444ec984f34d29a33f8572d92ca9d4b55479", + "browser_bundle": "Rebuilt on EC2 and copied locally; unchanged because server-side validation is excluded from the browser bundle.", + "source_integrity": "Changed files checked local versus EC2 by SHA256; source-checkpoint.json, commits.json and artifacts.sha256 in the checkpoint log directory identify exact sources, synchronized branch heads and binaries.", + "generation": "regen_generated.py --check --require-all passed; only shared conformance corpus changed, no generated record layout.", + "manifest_inventory": { + "audit_rows": 151, + "contract_areas": 20, + "pending_reviews": 0, + "focused_tests_passed": 11 + }, + "sdks": { + "go": { + "passed": 663, + "scope": "vgi and generated package suites; all shared schema vectors plus actual HTTP rejection/readback, MAP child metadata, endianness and registered-extension round trips", + "log": "sdk/go-regression.jsonl" + }, + "rust": { + "passed": 78, + "scope": "contract, dispatch and bounded stored-catalog CREATE lifecycle", + "log": "sdk/rust-regression.log" + }, + "typescript": { + "passed_per_backend": 65, + "skipped_per_backend": 41, + "assertions_per_backend": 724, + "skip_reason": "external-service/environment requirements", + "scope": "both Arrow backends; shared corpus and real CREATE/ADD/discovery dispatch", + "log": "sdk/typescript-regression.log" + }, + "java": { + "regression_passed": 655, + "skipped": 2, + "initial_failures": 1, + "correction": "Stale test-only protocol hash expectation refreshed to the unchanged current fingerprint; VgiProtocolSurfaceTest focused rerun passed 2/2.", + "logs": [ + "sdk/java-regression.log", + "sdk/java-hash-regression.log" + ] + }, + "csharp": { + "regression_passed": 660, + "initial_failures": 1, + "correction": "Stale test-only protocol hash expectation refreshed to the unchanged current fingerprint; VgiProtocolHashTests focused rerun passed 10/10.", + "logs": [ + "sdk/csharp-regression.log", + "sdk/csharp-hash-regression.log" + ] + }, + "cpp": { + "cases": 9, + "assertions": 227, + "log": "sdk/cpp-regression.log" + } + } + }, + "limits": [ + "Native1.5 ColumnDefinition exposes comments but no public tag accessor; tags remain in authoritative Arrow schema, not native SQL tag authoring/introspection. Reconstruction using only native columns cannot preserve tags or nested field metadata.", + "Generated-column execution/dependencies, optimizer contracts, broad provider qualification, protocol freeze/release and native2 engine port remain open.", + "The previously recorded nested NOT_NULL schema-reconciliation regression is outside this descriptive metadata checkpoint." + ], + "sdk_reader_fixes": [ + "Go recovers nested MAP key/value field metadata lost by the pinned Arrow IPC reader, retaining schema endianness, sorted-map flags and registered-extension semantics. MAP entries-field metadata that the Go API cannot represent rejects explicitly.", + "SDKs whose Arrow decoders normalize metadata validate raw schema metadata before duplicate keys or malformed UTF-8 can be erased.", + "Go and TypeScript declarative comment overrides preserve empty comments and existing field/schema metadata." + ], + "gate_effect": "Closes descriptive column comment/tag preservation and validation for the existing Arrow carrier with bounded native1.5 and six-SDK evidence. Virtual generated columns, optimizer work, remaining provider acceptance, P1-P3 freeze/release and M0-M4 native2 remain open.", + "sdk_revisions": { + "vgi-go": "d4fef8f319ae80dc87a3f4586c3ec14213bd833c", + "vgi-rust": "fb80c5755efcdf3484d6ed49a1eea74b7f5ce4cf", + "vgi-typescript": "3e6bb0c023653a69983ef0f3b3753b5b609bf2e8", + "vgi-java": "7bc58105c79e2917f9204798cd28291a3204872e", + "vgi-csharp": "5b2e090c02efef7590a8c19decea6ddf8692b7c0", + "vgi-cpp": "65a72eed971aa7ede4a181273685be5048398867" + } + }, + "virtual_generated_columns_checkpoint_2026_10_23": { + "date": "2026-10-23", + "status": "implemented_bounded_validation_passed", + "base_revisions": { + "vgi": "e45ec47634161fb8537f8f5c1b2af4e8d981db6d", + "vgi-python": "383912d77c25241af93d8aacfb31989efac7272f", + "vgi-go": "d4fef8f319ae80dc87a3f4586c3ec14213bd833c", + "vgi-rust": "fb80c5755efcdf3484d6ed49a1eea74b7f5ce4cf", + "vgi-typescript": "3e6bb0c023653a69983ef0f3b3753b5b609bf2e8", + "vgi-java": "7bc58105c79e2917f9204798cd28291a3204872e", + "vgi-csharp": "5b2e090c02efef7590a8c19decea6ddf8692b7c0", + "vgi-cpp": "65a72eed971aa7ede4a181273685be5048398867" + }, + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "contract": { + "carrier": "Existing Arrow vgi.column.generated.v1 canonical base64 SqlExpression IPC paired with vgi.column.generation.v1=virtual; containing field supplies resolved result type.", + "authoring": "Existing SQL-string convenience APIs translate locally with explicit owning schema and default evaluation context. No legacy generated_expression is published.", + "admission": "Validate canonical IPC, complete category/expression pair, owner, profile and no DEFAULT conflict. Legacy generated metadata and STORED reject before effects or publication. No representation capability opt-in.", + "execution": "Native DuckDB calculates virtual expressions; provider scans emit only physical fields in relative order. Native1.5 generated primitive grammar and default context remain bounded.", + "dependencies": "Reuse existing dependency authority, type binding, cycle checks and transaction snapshots. Unsupported semantic alterations reject unchanged; no new rewrite framework.", + "wire": "No new RPC, record layout, protocol version or engine-pin change." + }, + "validation": { + "status": "bounded_runtime_and_conformance_passed", + "host": "ec2-user@98.81.121.120", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "logs": "logs/generated-columns-20261023-X8dPoO", + "corpus": { + "valid_schemas": 3, + "invalid_schemas": 20 + }, + "python_regression": { + "passed": 1505, + "skipped": 9, + "log": "python-broad.log" + }, + "python_final_boundary": { + "passed": 52, + "log": "python-boundary-final.log", + "scope": "Includes final3/19 schema corpus, stored rejection, recursive legacy-only rejection and no-provider-effects/discovery checks." + }, + "python_lint": "Ruff passed; mypy224 sources passed. Format has only unchanged write_finalize_probe.py baseline.", + "python_final_corpus": { + "passed": 15, + "log": "corpus-final.log", + "scope": "Final3/20 vectors include malformed UTF-8 inside the SqlExpression IPC SQL value." + }, + "sdks": { + "go": { + "passed": 688, + "log": "sdk/go-regression.jsonl" + }, + "rust": { + "passed": 79, + "log": "sdk/rust-regression.log" + }, + "typescript": { + "passed_per_backend": 56, + "assertions_per_backend": 916, + "log": "sdk/typescript-regression.log" + }, + "java": { + "passed": 657, + "skipped": 2, + "log": "sdk/java-regression.log" + }, + "csharp": { + "passed": 663, + "log": "sdk/csharp-regression.log" + }, + "cpp": { + "cases": 10, + "assertions": 324, + "log": "sdk/cpp-regression.log" + } + }, + "sdk_native_queries": { + "queries_per_sdk": 5, + "sdk_count": 6, + "transports": "Subprocess all six; TypeScript also HTTP", + "scope": "Original generated value-query subset with per-name discovery; global catalog enumeration omitted because unrelated informational constraints are not qualified natively. Full original TypeScript test is not claimed passing.", + "logs": [ + "sdk/native-final-subset.log", + "sdk/native-final-subset/", + "sdk/typescript-native-http-final.log", + "sdk/native-http/", + "sdk/native-final-binaries.sha256" + ], + "binary": "Final native mapping/COUNT binary; all six subprocess subsets and TypeScript HTTP rerun after final link." + }, + "sdk_fixes": [ + "C#/TypeScript validate raw IPC UTF-8 before Arrow string normalization. Java bounds IPC message framing before Arrow allocation; malformed IPC initially reproduced a test-JVM OOM.", + "C# example scan now emits only physical n; Go example fields explicitly declare nullable storage instead of implicitly claiming NOT_NULL without constraints. Unsupported mutable examples reject before effects." + ], + "browser_bundle_sha256": "840df926c9b2ebbe63d6c480eed2444ec984f34d29a33f8572d92ca9d4b55479", + "browser_bundle": "Rebuilt on EC2 and copied locally; unchanged server-only helper code is excluded from browser bundle.", + "generation": "regen_generated.py --check --require-all passed; only conformance corpus changed among generated artifacts.", + "manifest_inventory": { + "audit_rows": 151, + "contract_areas": 20, + "pending_reviews": 0, + "tests_passed": 11 + }, + "native_build": "Static/loadable extension, haybarn, unittest and catalog-tests targets passed; final mapping/COUNT rebuild native-build-count.log.", + "native_contracts": { + "cases": 13, + "assertions": 260, + "log": "native-contracts.log", + "scope": "Four generated contract/corpus cases plus existing expression, metadata and default-schema checks." + }, + "native_lifecycle": { + "cli_probes": 24, + "successful_operations": 11, + "expected_rejections": 13, + "log": "native-lifecycle-3.log", + "artifacts": "native-lifecycle-3/verified.json", + "scope": "Native CREATE/public canonical readback, interleaved generated and physical columns, NULLs, COUNT/constant scans, hidden rowid, filter/projection/reconnect, builtin shadow isolation, warmed-context rejection, unchanged stored snapshots after invalid CREATE/ALTER/DML and CREATE rollback.", + "dml_limit": "Fixture rejects DML at its existing read-only planner check. This proves no provider effects; the additional ResolveWriteFunction generated-table backstop is source-reviewed, not exercised by these read-only DML probes." + }, + "native_regression": { + "sql_cases": 11, + "sql_assertions": 750, + "metadata_cases": 5, + "metadata_assertions": 183, + "logs": [ + "native-regression-final.log", + "native-metadata-regression.log" + ], + "scope": "Existing generated, defaults/comments/COPY, projection/filter, hidden rowid, partition, inlined scan and window self-join cases. Final SQL run has no skips." + }, + "protocol_hash": "457a2fc7c24a637480d2c34237ea0ccf826b10f4ea5e316d4623895f4a535585", + "protocol_hash_limits": "No RPC/record shape change. Canonical generated metadata supersedes legacy SQL strings inside Arrow schema payloads; fingerprint equality alone does not establish semantic compatibility.", + "source_integrity": "source-checkpoint.json records SHA256 equality for changed local/EC2 files across eight repositories; commits.json records synchronized task heads and artifacts.sha256 identifies final native binaries.", + "native_column_regression": { + "cases": 3, + "assertions": 251, + "log": "native-column-regression-final.log", + "correction": "Old generated-CREATE rejection expectation updated: native serialization now succeeds and the existing non-generated column provider rejects before effects. First run had that sole stale expectation; final three cases passed." + } + }, + "limits": [ + "Native1.5 parser rejects ALTER ADD generated columns. Stored/nested generation and native writes to generated tables are not qualified; reject before effects.", + "Durable row reference rejects semantic column ALTER unchanged. Earlier metadata-only dependency reference retains its separate qualified behavior.", + "General SQL/contexts/types, optimizer contracts, provider acceptance, P1-P3 freeze/release and M0-M4 native2 remain open.", + "Previously recorded nested NOT_NULL schema-reconciliation regression remains outside this checkpoint.", + "Native generated scans are qualified only for a single worker function; delegated/multiple-branch/partitioned generated scans reject pending separate proof." + ], + "sdk_revisions": { + "vgi-go": "bf0fffbedea6835bd8ac07875ef8e4d964b6c081", + "vgi-rust": "7008b4b84e602dd7523c28e5e4caaf70c944828a", + "vgi-typescript": "b90949ca9cef2e11063a78f11776c7e18c6b549b", + "vgi-java": "a26aabe8f380b62898bc2af1a0e5528b6c43a82e", + "vgi-csharp": "29f16d7b13d755dbf4eface70ad2ca2400b307f0", + "vgi-cpp": "b2860789b8d63aeff7945fd34654626dd6904fdf" + }, + "native_mapping": "Set generated category before physical indices; normalize logical scan IDs through existing table_entry at init, independently support engine-side projection when worker declines projection, map hidden rowid once, and expose a typed EMPTY virtual for count/constant scans. No new bind-data or cache identity field.", + "optimizer_limit": "Generated-table filter/order/column-statistics/late-materialization pushdown remains disabled; local SQL evaluation is qualified. Projection to a capable worker remains physical. Broader optimizer contracts are the next checkpoint.", + "gate_effect": "Closes the bounded canonical virtual CREATE/discovery/read checkpoint and shared generated-metadata migration. Optimizer proof, remaining provider/adapter acceptance, P1-P3 freeze/release and M0-M4 native2 remain open." + }, + "projection_expressions_2026_10_23": { + "status": "implemented_bounded_native15_and_all_sdk_reference_qualified_release_pending", + "contract": "docs/protocol/projection-expressions.md", + "scope": "Optional bound offer, pure proposal before optimizer replacement, per-expression decline, exact append-only result fields and complete replacement bind state.", + "wire": { + "records": [ + "TableFunctionProjectionRequest", + "TableFunctionProjectionResponse" + ], + "method": "table_function_projection", + "offer": "BindResponse.projection_expression_profiles", + "expressions": "Existing SqlExpression IPC; no custom scalar AST or metadata-only execution instruction.", + "mapping": "Strictly increasing accepted_indices select result_schema fields appended after unchanged source fields." + }, + "fallback": "No offer or valid decline keeps original scan/local expressions; malformed acceptance/RPC/execution failure is an error, with no execute-then-local retry.", + "native_boundary": "Ordinary non-generated catalog table scans and total INTEGER-to-BIGINT casts on pinned1.5; broader source shapes/expressions, cache/split identity and native2 APIs remain unqualified.", + "evidence": [ + { + "kind": "python", + "broad_passed": 1500, + "broad_skipped": 8, + "final_focused_passed": 188, + "quality": "Ruff and mypy passed; final generation/schema/inventory checks retained in task logs." + }, + { + "kind": "native_projection", + "cases": 4, + "assertions": 52, + "live_scenarios": 12, + "scope": "Actual accepted/partial/declined/unoffered traces, residual/reordered results, unsafe/context local fallback, malformed-before-init, no execution retry, fresh prepared execution, EXPLAIN with no scan, copied/serialized accepted state and stale-state rejection." + }, + { + "kind": "native_regressions", + "contract_cases": 21, + "contract_assertions": 245, + "sql_cases": 5, + "sql_assertions": 204, + "scope": "Prepared query, column/default/metadata/generated contracts; existing generated/projection/filter/cache SQL. No skips." + }, + { + "kind": "shared_public_rpc", + "cases_per_worker": 17, + "workers": [ + "python", + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "additional_transport": "TypeScript HTTP also passes17. Signed Python HTTP continuation covered by focused suite.", + "scope": "Typed exact metadata/results, partial/complete/context decline, accepted-state reuse, independent ordinary rebind,11 malformed request cases. Not complete transport framing conformance." + }, + { + "kind": "native_sdk_integration", + "workers": [ + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "queries_per_worker": 5, + "scope": "Widening results, residual/reordered/duplicate projections, local expression, two prepared executions, ordinary rebind; targeted catalog discovery avoids unrelated existing constraint enumeration failures." + }, + { + "kind": "sdk_regressions", + "details": "Java full664tests/2skips plus focused portable-state/outer checks; C# full670passed plus final295schema/hash/projection checks; Go contract/reflection/schema tests; Rust2projection+24client scans; TS types and focused Arrow/Flechette tests; C++10projection assertions. See sdk logs for exact final commands." + }, + { + "kind": "native_sdk_copy", + "workers": [ + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "status": "all_passed", + "scope": "Actual LogicalOperator::Copy serializes/deserializes accepted scans; retains schema/opaque state, cache/split exclusions and rejects stale original state. Final examples use targeted discovery to exclude unrelated existing constraints." + } + ], + "artifacts": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC/logs/expression-pushdown-20261023-VQa0w4", + "remaining": [ + "Go outer wrapped IPC framing/raw-size dependency qualification; no broad RPC dependency changes included here.", + "Bind-specific repeatability and mutable-provider proof", + "Broader projection adapter/grammar/identity qualification", + "Remaining P1-P3 provider/release gates", + "M0-M4 native DuckDB2 port" + ], + "published_protocol_unchanged": "vgi.v2/2.1.0", + "engine_pin_unchanged": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "native_revision": "e9bf8019ead7d01ab94f886b9a0ffd23af7e2257", + "protocol_hash": "41f61f37c7ce42f63f3ca47f8861508f3f356d2422a5b8a0bab48ff92c0857a8", + "protocol_methods": 85, + "browser_sha256": "6cca300e3de26d60eca2eb7a3f0449edf7875d6c1d9d4f7b8784bbad89ad9b77", + "native_lifetime": "Known offers disable prepared bind reuse before optimization. Copy retains accepted schema/state; serialized restore requires byte-identical original bind call and bind response, including opaque state. Java offered original binds now use stable invocation-scoped portable state rather than random pending-bind handles.", + "known_external_regression": "Existing unrelated catalog constraint/nullability enumeration mismatch remains separate. Native SDK queries/copy probes select the projection fixture without bulk catalog enumeration." + }, + "scan_repeatability_and_go_framing_2026_10_24": { + "status": "bounded_native15_and_sdk_qualified_release_pending", + "specification": "docs/protocol/scan-repeatability.md", + "wire_change": "Append required non-null dictionary BindResponse.repeatability with UNKNOWN/STABLE_WITHIN_QUERY; provider opt-in defaults UNKNOWN, no capability negotiation. Projection acceptance preserves exact value.", + "native_15": "Stores/copies claim; stable binds disable statement reuse and result cache, bypass stable inline bind metadata, and reject changed original state on serialized table-copy rebind. No new optimizer rewrite on 1.5.", + "native_20_remaining": "Port callback and table-backed scan classifier shortcut, preserving sampling/dynamic-filter conservatism.", + "go_rpc_dependency": { + "worktree": "vgi-rpc-go-duckdb2", + "branch": "codex/duckdb-2.0-qualification", + "fix": "Complete single-batch wrapped IPC consumption, valid physical EOF/legacy+modern EOS, rejection of extra batches/trailing/repeated/truncated messages; optional params-type raw byte limit checked before reader creation.", + "release_requirement": "SDK compile-time assertion requires new WrappedRequestSizeLimiter API. Publish and pin the paired dependency; isolated workspace replacement is for qualification only." + }, + "authored_evidence": [ + "Python mutable SQLite bound snapshot versus fresh-bind data, signed HTTP, two-turn continuation and projection preservation", + "Native decode/copy/serialized restoration, prepared execution, result-cache exclusion, stale inline metadata and recursive same-bind rescan", + "SDK default/stable enum codecs, invalid/null values, projection preservation and common immutable sequence fixture", + "Go dependency framing/early-limit unit tests; shared public RPC runner19cases, optional wrapped framing27cases" + ], + "validation": { + "status": "passed_with_explicit_limits", + "execution": "All builds and tests ran on EC2; edits and commits stayed in the isolated local worktrees.", + "native_build": "Static/loadable extensions, shell, unittest and vgi_catalog_tests passed against unchanged pinned engine.", + "native_contract_and_copy": { + "cases": 8, + "assertions": 100, + "logs": [ + "native-contract.log", + "native-copy.log" + ] + }, + "native_repeatability_live": { + "passed": 5, + "scope": "Fresh prepared snapshots, unoffered result-cache exclusion, stale inline bypass, projected prepared state, recursive repeated evaluation of one bound snapshot.", + "log": "native-live/verified.json" + }, + "native_projection_regression": { + "passed": 12, + "log": "projection-live.log" + }, + "native_explicit_remote_queries": { + "passed": 18, + "scope": "Typed parameters, ordering/empty output, autocommit/transaction prepared executions, release, refusal/error boundaries, rollback, early LIMIT and interruption; zero retained preparations/executions/fragments/attachments.", + "log": "native-query-regression.log", + "audit_artifacts": "native-query-scenarios" + }, + "native_sdk_queries_and_copies": { + "workers": [ + "go", + "rust", + "typescript", + "java", + "csharp", + "cpp" + ], + "query_shapes_per_worker": 5, + "all_passed": true, + "scope": "Widening, residual/reordered/duplicate projections, local expressions, prepared executions and ordinary rebind; actual serialized plan copies retain bound state. Targeted discovery excludes known unrelated bulk constraint enumeration issue." + }, + "python": { + "focused_passed": 47, + "broader_passed": 1340, + "skipped": 8, + "public_rpc_cases": 19, + "ruff": "pass", + "mypy_source_files": 224, + "logs": [ + "python-focused.log", + "python-broad.log" + ], + "format_limit": "Whole-repo formatting has unchanged baseline exception tests/catalog/write_finalize_probe.py; no unrelated reformat applied." + }, + "go_rpc": { + "top_level_tests_passed": 15, + "test_records_including_subtests": 30, + "vet": "pass", + "docs_verified_files": 18, + "logs": "sdk-go", + "workspace": "Qualification uses isolated vgi-go and vgi-rpc-go modules. docs-verify runs standalone with GOWORK=off; initial inherited workspace/tooling mismatch is not a source failure." + }, + "go_sdk": { + "top_level_tests_passed": 7, + "test_records_including_subtests": 29, + "subprocess_cases": 27, + "http_cases": 27 + }, + "rust": { + "focused_passed": 30, + "subprocess_cases": 27, + "additional_public_probe": "Rejected1-,2-,3-byte incomplete next-message prefixes after reproducing the original acceptance.", + "logs": "sdk-rust" + }, + "typescript": { + "type_build": "pass", + "focused_passed_per_backend": 14, + "backends": [ + "ArrowJS", + "Flechette" + ], + "default_subprocess_cases": 27, + "default_http_cases": 27, + "browser_build": "pass", + "logs": "sdk-ts" + }, + "java": { + "core_passed": 668, + "example_passed": 42, + "skipped": 2, + "subprocess_cases": 27, + "log": "java-full-2.log" + }, + "csharp": { + "passed": 679, + "subprocess_cases": 27, + "log": "csharp-full-2.log" + }, + "cpp": { + "ctest_passed": 72, + "subprocess_cases": 27, + "logs": [ + "cpp-ctest-final.log", + "cpp-projection-framing.log" + ] + }, + "generation": "Strict --check --require-all passed with all isolated roots supplied.", + "inventory": { + "audit_rows": 151, + "areas": 20, + "unresolved_reviews": 0 + }, + "review": "Independent source review covered native snapshot lifetime, Go framing and final Java/C#/Rust/TypeScript bounds; malformed dictionary values are covered by explicit NULL-bitmap regressions.", + "final_manifest_and_browser_regression": { + "passed": 13, + "log": "final-manifest-browser.log" + } + }, + "protocol_hash": "41f61f37c7ce42f63f3ca47f8861508f3f356d2422a5b8a0bab48ff92c0857a8", + "protocol_hash_note": "Canonical generator preimage unchanged because BindResponse is packed binary; nested DTO schema changes are not reflected in endpoint schema hash.", + "published_protocol_unchanged": "vgi.v2/2.1.0", + "engine_pin_unchanged": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "release_gates_unchanged": true, + "artifacts": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC/logs/repeatability-20261024-w3GOmK", + "engine_banner": "v1.5.6-dev149", + "ec2_host": "3.82.45.233", + "browser_sha256": "1c69d491c0de9a8f133ea047dc6b976b872b76806c4b709eaf8a297770af927b", + "source_checkpoint_revisions": { + "vgi": "dc4e7b0bf5fd6dd85bad61ee4ab6748a0ae699a7", + "vgi-python": "fe7f2dbd39ba4dbe85819454ecdbaaa840f6a45c", + "vgi-go": "ad5f89701ef0bc20845ae95bf1cffe0f8e6cbed8", + "vgi-rust": "1b8379f9222797b6ba93a5e851f96d9a3e2d062d", + "vgi-typescript": "35af7185611b298b3d1b35f2ab44051524fbc28b", + "vgi-java": "8127455e398b960ce8cd525c5cc331b8ca118bc3", + "vgi-csharp": "617f0161dd3f47ba9f8ae9dc4e105a05d5224e4b", + "vgi-cpp": "b9ac8fa825828493f94d87ba3295ecbca6a00e53", + "vgi-rpc-go": "2921723df8246e8715785ffccae4b1a0912d2550" + }, + "runtime_fixes": [ + "Java and TypeScript accept valid physical EOF after exactly one complete record batch.", + "C# rejects partial EOS before its permissive Arrow reader can treat it as EOF.", + "Rust checks the exact remaining stream suffix to reject partial length prefixes accepted by Arrow-Rust.", + "C++ expected reflection hash fixtures now match the unchanged canonical Python endpoint hash.", + "Native malformed-enum tests now include hidden NULL dictionary values retaining valid claim bytes." + ], + "remaining": [ + "Python generic RPC wrapper unwraps only the first outer batch before typed validation. The optional extra-batch probe fails (python-common-2.log); complete outer framing/raw-size rejection remains a release prerequisite.", + "TypeScript fullworker Flechette launch fails at catalog_attach on Binary schema backend comparison before reaching bind/projection (sdk-ts/subprocess27-flechette.log). Default ArrowJS subprocess/HTTP and focused newcode under both backends pass; Flechette fullworker transport remains unqualified.", + "Publish and pin the paired Go RPC dependency before release; qualification workspace is not a published dependency solution.", + "Broader production providers, state expiry/cleanup and transport/platform coverage; combined P1-P3 release gates and native DuckDB2 M0-M4 port remain open." + ] + }, + "automatic_query_pushdown_plan_2026_10_24": { + "status": "main_refreshed_native2_compile_inventory_recorded_automatic_not_implemented", + "design": "docs/protocol/vgi-catalog-query-pushdown-design.md#implementation-checkpoint--october-24-2026", + "review_inputs": { + "native": "38fe3c775bd332b9cd4b845f3518631ee6070955", + "python": "0566afd7f12a93dc8441db930ae85d764d35b570", + "engine_15": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "engine_20_source_audit": "e1784fa4231072d7152b8aaa20977bdb8ff4ace3", + "haybarn_head_observed_not_substituted_for_audit": "5ab8a76db12ad1ad63c4e4997bce37602a9d0b43", + "provenance": "Historical source review before the latest-main integration; see main_integration_checkpoint_2026_10_24 for merged inputs." + }, + "wire_decision": "Reuse existing automatic query records and prepare/release plus TABLE execution. No new wire change demonstrated by this review.", + "route": "Whole-SELECT early bound-plan optimizer using LogicalPlanSQLExporter::Export; preserve original plan until replacement validation. Parsed RemoteExecute remains disabled. ExportWithSources is conditional because it materializes replacement sources.", + "first_milestone": "Ordinary single-attachment INNER JOIN, predicate and GROUP BY COUNT with explicit ordering; one remote execution, no ordinary scan execution, final grouped rows returned, equality under the same snapshot and clean decline retaining the local plan.", + "checkpoints": [ + { + "id": "A0", + "work": "Exact 2.0 pin and isolated diagnostic build", + "depends_on": [], + "engineer_days": [ + 1, + 3 + ], + "status": "initial_compile_inventory_recorded_link_blocked", + "evidence": "native:docs/duckdb-2.0-a0-diagnostics.md", + "result": { + "native_cpp_sources": 107, + "compiled": 53, + "failed": 54, + "not_attempted": 0, + "additional_generated_c_units_compiled": 1, + "configure_exit": 0, + "build_exit": 1 + }, + "limits": "Compiler inventory only, first 12 errors per unit; shared declarations mask later failures. No engine/loadable link or runtime qualification. Full M0 remains open.", + "historical": true, + "superseded_by": "main_integration_checkpoint_2026_10_24.native20_refresh" + }, + { + "id": "A1", + "work": "Basic 2.0 catalog/scan and explicit-query port", + "depends_on": [ + "A0" + ], + "engineer_days": [ + 4, + 10 + ] + }, + { + "id": "A2", + "work": "Real SQL provider and shared backend snapshot proof", + "depends_on": [], + "engineer_days": [ + 4, + 8 + ], + "note": "May begin on 1.5 in parallel; certify chosen engine before A5." + }, + { + "id": "A3", + "work": "Bound-plan eligibility, export and output mapping", + "depends_on": [ + "A1" + ], + "engineer_days": [ + 3, + 6 + ] + }, + { + "id": "A4", + "work": "Native automatic preparation and verified replacement", + "depends_on": [ + "A2", + "A3" + ], + "engineer_days": [ + 4, + 8 + ] + }, + { + "id": "A5", + "work": "Bounded pilot qualification and observability", + "depends_on": [ + "A4" + ], + "engineer_days": [ + 5, + 10 + ] + } + ], + "estimates": { + "basis": "Source-review judgments, not measured delivery commitments. One experienced engineer, one DuckDB-backed provider, Linux ARM64; reuse current protocol/lifecycle. Re-estimate after A0/A1.", + "checkpoint_total_engineer_days_before_contingency": [ + 21, + 45 + ], + "first_end_to_end_demo_engineer_weeks": [ + 2, + 5 + ], + "qualified_pilot_engineer_weeks_with_contingency": [ + 5, + 10 + ], + "parallelism": "A2 can overlap A0/A1/A3; the first demonstration is an intermediate A2-A4 result, not additional effort.", + "latest_evidence": "After latest-main integration, all 110 native C++ sources were attempted: 54 compiled / 56 failed, plus generated describe.c compiled. The original 107 / 53 / 54 inventory remains historical. See main_integration_checkpoint_2026_10_24.native20_refresh. Estimates remain provisional; counts are not a completion percentage." + }, + "provider_evidence_limits": [ + "Finite explicit reference accepts six exact SQL strings over immutable data and creates fresh preparation/start connections; transaction token does not prove shared mutable data visibility.", + "Current fixture materializes before init and applies byte quota afterward; delayed fragment-read cancellation does not prove active backend SQL interruption.", + "Existing signed-HTTP ownership and fragment continuation tests remain valid; no claim that all HTTP query coverage is missing." + ], + "follow_up": [ + "Automatic typed parameters: pinned exporter rejects BOUND_PARAMETER; retain local execution initially and later preserve typed placeholders and fresh EXECUTE binding.", + "Wider relational/type/policy coverage after certification; not included in first pilot." + ], + "out_of_scope": [ + "Full nested-schema DDL, writes/COPY/advanced operators, all SDK SQL backends, WASM/platform distribution and full M0-M4 parity", + "New transaction framework, savepoints, durable replay/status services, query coordinator or separate snapshot registry", + "Protocol freeze, public version transition or completed P1-P3 qualification" + ], + "validation": "Original and refreshed audited-pin EC2 compile inventories exist; no automatic 2.0 runtime evidence. See native:docs/duckdb-2.0-a0-diagnostics.md and docs/duckdb-2.0-a0-main-sync.json.", + "enablement": { + "worker_opt_in": "ClientQueryCapabilities offers encoding/profiles and client engine build; worker QueryCapabilities selects automatic mode and vgi.duckdb.relational.v1 only when compatible and transaction support exists.", + "client_default": "Qualified 2.0 adapter automatically considers eligible queries when compatible automatic support is discovered. No user/session control; the current 1.5 explicit-query gate does not govern the future automatic path.", + "absent_or_explicit_only": "Keep ordinary local plans and send no automatic preparation RPC.", + "wire_change": "None; reuse the existing capability offer/selection and per-query preparation decision.", + "per_query_decision": "Eligibility checks and provider preparation acceptance remain mandatory; a valid decline retains the original local plan." + } + }, + "main_integration_checkpoint_2026_10_24": { + "status": "integrated_with_known_preexisting_native15_limitation", + "summary": "Merged published main into the combined successor work without changing the pinned 1.5 engine or implementing automatic whole-query pushdown.", + "document": "docs/protocol/duckdb-2.0-main-integration-2026-10-24.md", + "published_baseline": "vgi.v2/2.2.0", + "development_placeholder": "vgi.v2/2.2.0; incompatible unpublished successor, not a compatible published 2.2 update", + "final_major_and_routing": "unresolved; major-required decision and coordinated release gates unchanged", + "merged_refs": { + "vgi": { + "base": "e1dc7520d28dec8cc7414bb65e227e565b6ff560", + "main": "454fefa50b11f8bfe3065fb11aebf8f5761d7b3d" + }, + "vgi-python": { + "base": "7dc87f44bb73c723b3cac13f059af50338fdd284", + "main": "fd4f5b97a41972475d93e627c1688b4e75712f6d" + }, + "vgi-go": { + "base": "ad5f89701ef0bc20845ae95bf1cffe0f8e6cbed8", + "main": "ca85f8cfef3eb8c17bbbb6a13f8a48bf1956670e" + }, + "vgi-rust": { + "base": "2f0c343ebf010f1cbf6b37f0d084ab8a7ccca338", + "main": "89f3cf651badf1f9caccc89f43f208fa34770e59" + }, + "vgi-typescript": { + "base": "977b667f49c924f912127eae2c8957d3d0658fa6", + "main": "412384aa56d96d19fdbc88de42a94d994b70ca88" + }, + "vgi-java": { + "base": "10613b873996fb8bf755a21c80ffd0bab713ec69", + "main": "074ec2fc13973479504bec12f07fb7a597c5343e" + }, + "vgi-csharp": { + "base": "d77b0b209a8db9fecb220d7d575932d51b54106d", + "main": "15d3cae296d9082e68ad36c058731cb7d8bde7ff" + }, + "vgi-cpp": { + "base": "1da952dd328578ee9695f50e3ae719e9e57a511f", + "main": "0c5e24522690119bc002f423130be266c8a010a5" + }, + "vgi-rpc-go": { + "base": "2921723df8246e8715785ffccae4b1a0912d2550", + "main": "688ba348b67c47125953e98ee74847efc279cd87" + }, + "vgi-rpc-java": { + "base": "30c84da7fc20b38f7c59aae5d2beb5ee483be6a1", + "main": "3a8a300a10226be1b56e0458e81ffa3212b73f1d", + "tag": "v0.33.1" + }, + "vgi-rpc-c++": { + "base": "08951ccd7ebd4661655765e733388ce371894a7a", + "main": "25fad89ed4bc7746e2958537752a00fe0a707ecd", + "tag": "v0.11.0" + } + }, + "backup_ref": "codex/pre-main-sync-20261024-kmz7l4hp", + "dependency_versions": { + "python": { + "vgi-rpc": "0.52.0", + "source": "published locked package" + }, + "typescript": { + "vgi-rpc": "0.29.2", + "flechette": "2.7.1", + "apache-arrow": "21.1.1", + "source": "published packages" + }, + "rust": { + "vgi-rpc": "0.32.0", + "vgi-rpc-client": "0.32.0", + "vgi-rpc-macros": "0.32.0", + "source": "published crates" + }, + "go": { + "vgi-rpc-go": "0.35.0", + "source": "isolated merged source replacement; retained wrapped-request framing patch still requires a published/pinned release" + }, + "java": { + "vgirpc": "0.33.1", + "source": "isolated tag merge retaining the annotated String dictionary fix" + }, + "csharp": { + "QueryFarm.VgiRpc": "0.18.1", + "source": "published NuGet package; tests use -p:VgiRpcCSharpRoot=/nonexistent to bypass the unrelated old 0.16.0 EC2 sibling" + }, + "cpp": { + "vgi_rpc": "0.11.0", + "source": "isolated tag merge retaining local strict decoding fixes" + } + }, + "reconciled_overlaps": [ + "Six catalog-wide metadata streams retain complete canonical constraints, typed index definitions, macro overload families, schema options and column metadata; SchemaInfo attachment state is constructor-only, not on the wire.", + "Python selected capability projection participates in cached inventory, publication identity and derived etags; unscoped validator shortcuts cannot answer projected inventory. SnapshotRequest.capability_projection is internal only, not a new wire field.", + "Generated C++ request/response codecs replace duplicate hand-maintained schema inventories while retaining successor methods and strict nested semantics.", + "Published Identity.v2 timestamp signatures and continued table-in-out output coexist with successor projection/repeatability/query contracts.", + "Java bulk metadata uses canonical IndexInfo and retains explicit annotated String dictionaries alongside upstream binary dictionaries. All three newly merged bulk SDK codecs retain canonical table column validation.", + "C# unpublished local main ce20f46 snapshot work is covered by pinned origin/main including 1badcaf and later fixes; no duplicate cherry-pick was needed." + ], + "validation": { + "artifacts": "logs/main-sync-20261024-kmz7l4hp", + "sdk": { + "typescript": { + "arrowjs": { + "passed": 738, + "skipped": 68, + "log": "sdk/typescript-unit-final.log" + }, + "flechette": { + "passed": 739, + "skipped": 67, + "log": "sdk/typescript-flechette-1.log" + }, + "production_build": "passed sdk/typescript-build-1.log", + "common_projection_subprocess": 27, + "common_projection_signed_http": 27 + }, + "java": { + "core": { + "passed": 739, + "skipped": 2 + }, + "examples": { + "passed": 61 + }, + "rpc": { + "passed": 681 + }, + "logs": [ + "sdk/java-full-3.log", + "sdk/java-rpc.log" + ], + "common_projection_subprocess": 27, + "common_projection_signed_http": 27 + }, + "csharp": { + "passed": 764, + "skipped": 0, + "log": "sdk/csharp-full-3.log", + "common_projection_subprocess": 27, + "common_projection_signed_http": 27 + }, + "go": { + "passed": 881, + "skipped": 1, + "skip": "TestCfDoStorage_Live", + "log": "sdk/go-tests-final.jsonl", + "vet": "passed", + "common_projection_subprocess": 27, + "common_projection_http": 27 + }, + "go_rpc": { + "passed": 766, + "environment_skips": 17, + "log": "sdk/rpc-go-tests.jsonl", + "python_enabled_reference_subset_passed": 36, + "python_to_go_server_conformance_passed": 1696, + "additional_logs": [ + "sdk/rpc-go-python-final.jsonl", + "sdk/rpc-go-conformance-final.log" + ], + "vet": "passed" + }, + "rust": { + "passed": 664, + "ignored": 4, + "test_binaries": 45, + "log": "sdk/rust-tests-built-example.log", + "common_projection_subprocess": 27 + }, + "cpp": { + "ctest_passed": 89, + "ctest_total": 89, + "log": "sdk/cpp-tests-final.log", + "common_projection_subprocess": 27 + } + }, + "native15": { + "engine": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "build": "static extension, loadable extension, shell, unittest, catalog tests and HTTP tests passed", + "artifact": "native-merge-evidence.json", + "contracts": { + "cases": 74, + "assertions": 1542 + }, + "http": { + "cases": 8, + "assertions": 265 + }, + "catalog_multibatch": { + "cases": 6, + "assertions": 380 + }, + "continuation": { + "cases": 5, + "assertions": 473 + }, + "discovery_reflection": { + "cases": 8, + "assertions": 221 + }, + "copy": { + "cases": 2, + "assertions": 41 + }, + "live_repeatability_cases": 5, + "live_projection_cases": 12, + "live_explicit_query_cases": 18, + "six_sdk_native_projection": { + "sdks": 6, + "queries_per_sdk": 5, + "rows_per_sdk": 42, + "accepted_plan_copy": "passed for all six" + }, + "full_subprocess_integration": { + "status": "completed_one_preexisting_failure", + "current_tracked_files": 379, + "passed": 346, + "skipped": 32, + "failed": 1, + "log": "native-full.log", + "summary": "native-full.csv", + "raw_run": { + "files": 380, + "passed": 347, + "skipped": 32, + "failed": 1, + "seconds": 500.8 + }, + "inventory_note": "Raw run also executed the retired parallel_2gb.test_slow (passed). Main renamed it to parallel_spill_stream.test_slow. The exact old file was archived outside the source tree; counts above exclude it.", + "known_failure": { + "test": "test/sql/integration/schema_reconcile.test:40", + "error": "native1.5 does not support nested constraint column paths", + "status": "reproduced_preexisting_adapter_limitation", + "confirmation_log": "schema-reconcile-confirmation/result.log", + "evidence": [ + "Premerge native e1dc752 already rejects constraint paths with more than one component.", + "Premerge Python 7dc87f44 schema_reconcile fixture already publishes nested.a NOT_NULL.", + "SQL test unchanged; prior matching runtime failure documented in native docs/duckdb-2.0-plan.md." + ], + "limits": "No new nested-constraint support or fixture weakening was included in this integration.", + "resolved_by": "nested_not_null_checkpoint_2026_10_24" + } + } + }, + "python": { + "status": "passed", + "initial_full": { + "passed": 4596, + "skipped": 129, + "failed": 4, + "log": "python-full-1.log" + }, + "fixed_followups": [ + "Docstring-only gate: 1 pass python-docstrings.log", + "Protocol inventory, native registry and browser regression: 13 passed", + "Mypy: 235 source files passed" + ], + "final_full": { + "passed": 4602, + "skipped": 129, + "failed": 0, + "warnings": 6, + "seconds": 79.54, + "log": "python-full-final.log" + }, + "http_fixture_regression": { + "cause": "Test server advertised a free port before binding; parallel startup could query another server. Bind port 0 before publishing its port and check child liveness.", + "targeted_passed": 19, + "repeated_parallel_passed": 57, + "logs": [ + "demo-storage-fixed-1.log", + "demo-storage-repeat-1.log", + "demo-storage-repeat-2.log", + "demo-storage-repeat-3.log" + ] + }, + "mypy": { + "source_files": 235, + "status": "passed", + "log": "python-mypy-final.log" + } + }, + "generation": { + "status": "passed", + "all_repositories_required": true, + "log": "generation-check.log" + }, + "inventory": { + "status": "passed", + "audit_rows": 151, + "contract_areas": 20, + "unresolved_scope_reviews": 0, + "log": "manifest-final.log", + "limits": "Coverage and selected scope decisions, not phase completion or protocol freeze." + } + }, + "native20_refresh": { + "status": "compile_inventory_recorded_link_blocked", + "native_cpp_sources": 110, + "compiled": 54, + "failed": 56, + "not_attempted": 0, + "additional_generated_c_units_compiled": 1, + "captured_capped_errors": 415, + "artifacts": [ + "native:docs/duckdb-2.0-a0-main-sync.json", + "native:docs/duckdb-2.0-a0-diagnostics.md#main-integration-refresh--october-24-2026" + ], + "changes": [ + "Two new native codec sources compile.", + "New http_params fails FileOpener setting overrides.", + "Existing input_dedup now fails removed DataChunk::SetCapacity.", + "Other existing source statuses unchanged." + ], + "historical_original": { + "native_cpp_sources": 107, + "compiled": 53, + "failed": 54 + }, + "limits": "Compile inventory only; capped errors are not independent fixes. No engine/loadable link, native 2 runtime or automatic optimizer qualification.", + "engine_revision": "e1784fa4231072d7152b8aaa20977bdb8ff4ace3", + "source_manifest_sha256": "7cd27e1f647d1cd52eddfe724e76f26c6d08429864889a2df6570db4627e80de" + }, + "protocol_hash": "41f61f37c7ce42f63f3ca47f8861508f3f356d2422a5b8a0bab48ff92c0857a8", + "protocol_methods": 85, + "hash_limits": "Endpoint hash does not cover every nested binary record or semantic metadata rule.", + "browser_sha256": "2b49411f93ff4f538dcbec6c767c663c56026b11f5e5595c0fb749e4743dc6e8", + "historical_evidence_policy": "Earlier dated checkpoints, version fields, source revisions and initial A0 counts describe their original inputs. They are retained as historical evidence and are not silently requalified by this merge.", + "remaining": [ + "Native 2.0 read-path port after refreshed compile inventory", + "General SQL provider, shared mutable snapshot and active-SQL cancellation proof", + "Automatic whole-query eligibility/export/replacement; no new wire requirement demonstrated", + "Paired dependency publication/pinning, remaining transports/platforms and P1-P3 release qualification" + ], + "gate_effect": "Integration checkpoint only; no release, freeze, full DuckDB 2 support or automatic whole-query implementation claim.", + "merged_commits_except_this_document": { + "vgi-go": "136f1e9777953de31de1da6264565f8e148ebb68", + "vgi-rust": "d8de99d4fbd5e40c72cff4d18f0fadb95300da75", + "vgi-typescript": "d6fbcd2350b2706b97e8994aae40ee07e8280d0d", + "vgi-java": "14f141c0dedf38359fc727d010511deb3dbb5f74", + "vgi-csharp": "9af874c5aeda84845208a7c7d8ca5a7287d3bf94", + "vgi-cpp": "50c5bc93ee2343c88324995dba89d41e31a15aec", + "vgi-rpc-go": "47f03a9f054dd0b2424817221c6f800f5427c445", + "vgi-rpc-java": "3c9ed089f0d197ed7691e724dcfb52a5ca554ea5", + "vgi-rpc-c++": "3ee63c04a32b1bad75d722467ada25760c16ba7d", + "vgi": "95df719667a5e226f6c82cc13d643a2a13207d08" + }, + "superseded_native15_failure_by": "nested_not_null_checkpoint_2026_10_24" + }, + "nested_not_null_checkpoint_2026_10_24": { + "status": "implemented_and_qualified_on_native15", + "summary": "Native 1.5 imports enforced ALL_ROWS nested STRUCT NOT_NULL facts while preserving nullable parents and canonical identities; Python and Go reconciliation writes reject invalid nested values.", + "wire_change": false, + "sources": [ + "native:src/vgi_constraint_contract.cpp", + "native:test/cpp/test_constraint_contract.cpp", + "native:test/sql/integration/schema_reconcile.test", + "go:examples/schema_reconcile/constraints.go", + "go:examples/schema_reconcile/constraints_test.go", + "go:vgi/attach_capabilities_test.go" + ], + "semantics": [ + "Exact raw path resolution with ambiguity, depth and synthetic/storage-target rejection.", + "Arrow nullability agrees recursively with trusted facts through ordinary STRUCT fields. List and logical-type storage children are not SQL paths.", + "Only top-level NOT_NULL facts enter native indices and ALTER identity maps. Required children never make their nullable ancestors required.", + "Provider enforcement remains required for writes. No new nested UNIQUE/PRIMARY_KEY/FK, nested SQL constraint DDL, generic enforcement or transaction guarantee.", + "Go bounded fixture validates actual stored rows before ALL_ROWS and validates insert/update arrays before effects; its attach selection intersects existing offers/prerequisites. The generic hook only forwards provider-owned capability records." + ], + "validation": { + "native_build": "static/loadable extension, shell, unittest and catalog tests passed on the retained 1.5 pin", + "native_constraints": { + "cases": 14, + "assertions": 480, + "log": "nested-not-null-20261024/native/constraints-final.log" + }, + "python_native_subprocess": { + "cases": 1, + "assertions": 64, + "log": "nested-not-null-20261024/native/schema-reconcile-subprocess.log" + }, + "python_native_http": { + "cases": 1, + "skipped": 0, + "log": "nested-not-null-20261024/native/schema-reconcile-http.log" + }, + "python_contracts": { + "passed": 85, + "skipped": 0, + "log": "nested-not-null-20261024/python-contracts.log" + }, + "go_full": { + "passed": 887, + "skipped": 1, + "skip_reason": "TestCfDoStorage_Live requires an external service", + "vet": "passed", + "log": "nested-not-null-sdk-20261024-gKrJCb/go-full.jsonl" + }, + "go_native_subprocess": { + "cases": 1, + "assertions": 64, + "skipped": 0, + "log": "nested-not-null-sdk-20261024-gKrJCb/go-native.log" + }, + "go_native_http": { + "cases": 1, + "assertions": 64, + "skipped": 0, + "log": "nested-not-null-sdk-20261024-gKrJCb/go-native-http.log" + }, + "rust_targeted": { + "passed": 21, + "log": "nested-not-null-sdk-20261024-gKrJCb/rust.log" + }, + "cpp_sdk_targeted": { + "passed": 4, + "log": "nested-not-null-sdk-20261024-gKrJCb/cpp.log" + }, + "typescript_targeted": { + "arrowjs_passed": 65, + "flechette_passed": 65, + "skipped": 0 + }, + "java_targeted": { + "core_passed": 40, + "example_passed": 7, + "skipped": 0 + }, + "csharp_targeted": { + "passed": 45, + "skipped": 0, + "rpc": "published 0.18.1" + }, + "managed_sdk_evidence": "nested-not-null-20261024/sdk-audit-VtU0JX/results.json", + "scope": "Same native write regression qualified with Python and Go only. Remaining SDK checks establish record preservation and explicit refusal of unsupported constrained mutations; they do not claim provider enforcement.", + "full_native": { + "status": "passed", + "log": "nested-not-null-20261024/native/full.log", + "summary": "nested-not-null-20261024/native/full.csv", + "files": 379, + "passed": 347, + "skipped": 32, + "failed": 0, + "seconds": 376.25, + "skip_scope": "Optional services/extensions and transport-specific fixtures; skipped files are not passes." + } + }, + "limits": "Native DuckDB 2 port, automatic whole-query pushdown, protocol freeze and release qualification remain open.", + "go_revision": "4dcba2ac1d5cf2dd219bfd7598f8b75d89d01fee" + }, + "full_language_slt_checkpoint_2026_10_24": { + "status": "qualified_configured_native15_matrix", + "wire_change": false, + "engine_pin_changed": false, + "native_pin": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "native_unittest_sha256": "98842b7122ad5fb2f07c04f5d551bc87eb5103a961d948529e0c849088fe440e", + "remote_log_root": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC/logs/full-language-slt-20261024-75jtiyf_", + "lanes": { + "python": { + "files": 389, + "passed": 356, + "skipped": 33, + "failed": 0, + "transport": "launcher", + "floor": 300, + "log": "python/final-stable/full.log", + "crash_passed": 2 + }, + "go": { + "files": 390, + "passed": 358, + "skipped": 32, + "failed": 0, + "transport": "subprocess", + "floor": 305, + "log": "go/final-qualified/full.log", + "crash_in_primary": true + }, + "rust": { + "files": 379, + "passed": 342, + "skipped": 37, + "failed": 0, + "transport": "subprocess", + "floor": 290, + "log": "rust/final/full.log", + "crash_in_primary": true + }, + "typescript_arrowjs": { + "files": 388, + "passed": 342, + "skipped": 46, + "failed": 0, + "transport": "launcher", + "floor": 290, + "log": "typescript/arrowjs-final3", + "crash_passed": 2 + }, + "typescript_flechette": { + "files": 388, + "passed": 342, + "skipped": 46, + "failed": 0, + "transport": "launcher", + "floor": 290, + "log": "typescript/flechette-final3", + "crash_passed": 2 + }, + "java": { + "files": 373, + "passed": 336, + "skipped": 37, + "failed": 0, + "transport": "launcher", + "floor": 295, + "log": "java/final3", + "crash_passed": 2 + }, + "csharp": { + "files": 379, + "passed": 342, + "skipped": 37, + "failed": 0, + "transport": "launcher", + "log": "csharp/final3", + "crash_passed": 2, + "floor": 318 + }, + "cpp": { + "files": 379, + "passed": 341, + "skipped": 38, + "failed": 0, + "transport": "subprocess", + "floor": 290, + "log": "cpp/final-stable/full.log", + "crash_in_primary": true + } + }, + "notes": [ + "Pass and skip counts are SQL files, not individual assertions.", + "Full configured primary lanes; expected skip and minimum-executed gates retained. Dedicated fixture/reference probes run separately.", + "Launcher lanes exclude destructive worker-crash tests and run their two-file subprocess lane separately.", + "C++ is qualified separately because native make test_languages currently includes only six SDK targets.", + "This does not qualify all HTTP/external services/platforms, native DuckDB 2 runtime or the unpublished breaking protocol release." + ], + "native_contracts": { + "cases": 12, + "assertions": 165, + "failed": 0 + }, + "reference_sql": { + "column_paths_required_filters_view_aliases": { + "files": 3, + "assertions": 252, + "failed": 0 + }, + "schema_dependencies": "passed", + "default_schema": "passed" + }, + "release_artifacts": { + "loadable_extension": "built", + "shell": "built", + "cli_load_and_queries": "passed (10 rows and product 42)" + }, + "sdk_suites": { + "rust": { + "passed": 669, + "ignored": 4 + }, + "cpp": { + "passed": 93, + "skipped": 0 + }, + "typescript_arrowjs": { + "passed": 741, + "skipped": 68 + }, + "typescript_flechette": { + "passed": 742, + "skipped": 67 + }, + "java_core": { + "passed": 739, + "skipped": 2 + }, + "java_example": { + "passed": 63, + "skipped": 0 + }, + "csharp": { + "passed": 766, + "skipped": 0 + }, + "go": { + "passed": 908, + "skipped": 1, + "vet": "passed", + "log": "go/sdk-final.jsonl", + "skip_reason": "TestCfDoStorage_Live requires external service" + }, + "python": { + "scope": "Production code unchanged in this checkpoint; prior full suite4602pass129skip retained as historical evidence. Changed diagnostic workers qualify through the native reference SQL and Ruff." + } + }, + "fixes": [ + "Exact single SQL-file registration selection; missing/ambiguous registrations fail.", + "Complete Go inline BindResult encoding and truthful nullable ordinary fixture fields.", + "Immutable constraint facts validated against scan rows, explicit read-profile offer selection, and version-owned snapshots.", + "Native nonrecursive CTE/set-operation and AT macro qualification, preserving caller ownership and invocation context guards.", + "Plain-table lookup declines eager neighboring expression materialization in unsupported contexts.", + "Malformed diagnostic fixtures corrupt current item/category streams; direct bulk framing tests retain existing optional-discovery fallback." + ], + "gate_effect": "No main merge or release approval. Breaking successor version/routing decision, P1-P3 release qualification, native2 M0-M4 and automatic whole-query implementation remain open.", + "diagnostic_scope": "Required-filter hostile metadata covers get/list paths. View security covers get/list and bulk: direct diagnostics for malformed collection schema/nullability, seeded materialization for semantic barrier/unknown modes. No required-filter bulk qualification claim." + }, + "explicit_statement_checkpoint_2026_10_25": { + "status": "bounded_python_reference_and_existing_native_read_regression_qualified", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "host": "ec2-user@18.234.89.245", + "logs": "logs/remote-statements-20261025-2ka4f9jv", + "wire_delta": "New optional vgi.remote.statement.v1 / statement profile and vgi.statement.result.v1 schema metadata. Existing command affected_rows Arrow row and RETURNING output stream; no RPC or record-layout changes. Generated service descriptions updated across six SDKs; executable statement implementation only in Python reference.", + "native_boundary": "Native vgi_query retains readonly-only negotiation; no SQL mutation entry point or DuckDB2 adapter enablement.", + "qualification_limits": [ + "Finite eight-statement DuckDB1.5.5 reference in one provider process; no general SQL sandbox or cross-process transaction owner routing.", + "Cancellation tests occur after backend materialization; active-SQL interruption is unqualified.", + "Lost-init test simulates an application boundary failure, not packet loss or crash recovery.", + "Expiry is exercised through explicit or request-time sweep; no unattended production reaper.", + "Automatic query pushdown, native mutation scheduling/cache invalidation, full transport/platform matrix, protocol version/freeze/release remain open.", + "Other SDKs received generated service description/content-hash updates only; their full unit and language SQL matrices were not repeated. Prior October24 language qualification remains historical. No native code or engine pin changed." + ], + "validation": { + "python_full": { + "passed": 4646, + "skipped": 129, + "warnings": 8, + "command": "pytest -q -n 8", + "log": "python-full-qualified.log" + }, + "native_read_queries": { + "passed_scenarios": 18, + "command": "make test_queries", + "log": "native-read-query.log" + }, + "real_socket_example": { + "passed": true, + "command": "pytest -q tests/catalog/test_statement_socket.py", + "log": "socket-example.log" + }, + "generated_artifacts": { + "status": "pass", + "command": "python scripts/regen_generated.py --check --require-all --root vgi-c++=$VGI_CPP_ROOT", + "log": "generated-check-qualified.log" + }, + "mypy": { + "status": "pass", + "source_files": 3, + "log": "mypy.log" + }, + "ruff": { + "status": "pass", + "formatted_files": 9 + }, + "manifest": { + "audit_rows": 151, + "areas": 20, + "unresolved_scope_decisions": 0 + }, + "protocol": { + "methods": 85, + "endpoint_schema_hash": "41f61f37c7ce42f63f3ca47f8861508f3f356d2422a5b8a0bab48ff92c0857a8", + "schema_change": false + } + }, + "diagnostics": "Earlier python-full.log reports six generated service file-content hash mismatches; exact rendered documentation headers were corrected. Final full suite and strict generator check pass. These file-content hashes are distinct from the unchanged endpoint schema hash." + }, + "python_rpc_framing_checkpoint_2026_10_27": { + "status": "bounded_single_value_framing_qualified_publication_pending", + "host": "ec2-user@54.224.166.207", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "artifacts": "qualification/python-framing-20261027-zJPaPY", + "sources": { + "rpc_branch": "codex/single-record-ipc-framing", + "rpc_revision": "99b3090cade6057ae9639fbebde1b3135e336535", + "rpc_base": "f616b98", + "vgi_python_revision": "f6eb3b4f8aaa84a7ae41dba0f0db8dea14f284e3", + "native_revision": "1e5822cba8ad87abe94ec3b56d7bd64f6da2e708", + "engine_pin": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "native_unittest_sha256": "98842b7122ad5fb2f07c04f5d551bc87eb5103a961d948529e0c849088fe440e", + "native_extension_sha256": "12da181827b912c397693eb3b32f831a7844a006bc8781817f4ca818b362487b", + "file_hashes": "vgi-logs/source-provenance.json" + }, + "change": "RPC typed parameters/unary results honor the existing deserialize_from_bytes hook. Default bounded decoding requires one complete batch and exact physical EOF or legacy/modern EOS; empty/schema-only utility values remain accepted. VGI raw8MiB checks run before Arrow decoding. Normalize truncated in-memory Arrow body OSError at each bounded decoder; malformed requests return HTTP400 and generic corrupt unary results retain TransportError. No streaming framing, wire fields, capability or engine pin change.", + "environment": { + "python": "3.14.7", + "rpc_suite_pyarrow": "26.0.0", + "vgi_suite_pyarrow": "25.0.1", + "vgi_duckdb": "1.5.5", + "dependency_selection": "Dedicated frozen RPC environment; existing VGI environment uses explicit PYTHONPATH source substitution. Its installed vgi-rpc0.52.0 is unchanged. No publication or dependency pin update claimed." + }, + "validation": { + "baseline": { + "rpc_parent": "Three extra-batch tests fail as expected, across every IpcValidation mode.", + "vgi_published_rpc": "Public projection runner with installed RPC0.52.0 rejects qualification: malformed extra IPC batch is accepted.", + "logs": [ + "rpc-baseline.log", + "vgi-logs/baseline-subprocess.log" + ] + }, + "rpc_full": { + "passed": 5736, + "skipped": 19, + "warnings": 59, + "seconds": 107.07, + "command": "python -m pytest -n 4 --dist loadfile --durations=15", + "log": "rpc-full.log" + }, + "vgi_python_full": { + "passed": 4661, + "skipped": 129, + "warnings": 4, + "seconds": 69.71, + "command": "pytest -q -n 4", + "log": "vgi-logs/python-full-final.log" + }, + "public_projection": { + "standard_cases_per_transport": 27, + "extended_cases_per_transport": 37, + "transports": [ + "subprocess", + "real localhost HTTP" + ], + "additional_cases": "All seven incomplete modern EOS lengths and all three incomplete legacy EOS lengths.", + "log": "vgi-logs/projection-qualified.log", + "results": "vgi-logs/projection-summary.json" + }, + "http_status": { + "responses_checked": 26, + "result": "All valid wrapped requests returned200 and all malformed/oversized wrapped requests returned400.", + "log": "vgi-logs/http-status-summary.json" + }, + "native_read_queries": { + "passed_scenarios": 18, + "command": "make test_queries", + "log": "native-queries.log" + }, + "static_checks": { + "status": "passed", + "scope": "RPC full pytest includes Ruff/mypy; focused RPC production mypy and formatting pass. VGI changed files pass Ruff/format and typed_values mypy." + }, + "native_python_slt": { + "passed_files": 356, + "expected_skips": 33, + "crash_recovery_passed_files": 2, + "gate_status": "minimum-executed and original skip allowlist passed; zero failures", + "command": "make test_python with explicit fixture commands; see run-native.sh", + "log": "native-python-slt.log" + }, + "manifest": { + "audit_rows": 151, + "areas": 20, + "unresolved_scope_decisions": 0, + "log": "manifest-check.log" + } + }, + "gate_effect": "Supersedes the Python outer IPC framing/raw-size qualification gap recorded October24. Publish/pin both tested Python and Go RPC dependencies before release. No main merge, protocol freeze or DuckDB2 port qualification.", + "limits": [ + "Only Python SDK/native Python-worker integration is repeated for this dependency fix; the October24 other-language SQL matrix remains historical.", + "Bounded single-value decoding and projection request checks are qualified on Linux ARM64 with the recorded Arrow versions. This is not a complete platform/transport matrix.", + "Final successor protocol identity, remaining P1-P3 release/provider gates and native2 M0-M4 remain open." + ], + "publication_followup": "Completed by rpc_publication_checkpoint_2026_10_27; this earlier source-override checkpoint remains historical evidence." + }, + "rpc_publication_checkpoint_2026_10_27": { + "status": "rpc_fixes_merged_released_and_sdk_dependencies_pinned", + "host": "ec2-user@54.224.166.207", + "workspace": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC", + "python": { + "package": "vgi-rpc", + "version": "0.52.3", + "pull_request": "https://github.com/Query-farm/vgi-rpc-python/pull/76", + "release": "https://github.com/Query-farm/vgi-rpc-python/releases/tag/v0.52.3", + "merge_revision": "2daca0968af44c8d503d2568ada8a79362f265cd", + "sdk_pin_revision": "2bd835c8d7e1686d4ba18e41de94d5db20af5c11", + "ci": "Linux/macOS/Windows tests, lint, docs and CodeQL passed before merge; PyPI publication workflow 40568100712 and post-merge CI 40568112064 passed.", + "artifacts": "qualification/rpc-release-python-20261027", + "installed_wheel_regressions": { + "passed": 112, + "log": "pypi-tests.log" + }, + "sdk_full": { + "passed": 4661, + "skipped": 129, + "warnings": 4, + "log": "sdk-full-final.log" + }, + "sdk_slt": { + "passed_files": 356, + "expected_skips": 33, + "crash_recovery_passed": 2, + "log": "native-python-slt.log" + }, + "native_query_scenarios": { + "passed": 18, + "log": "native-queries.log" + }, + "public_projection": { + "standard_per_transport": 27, + "extended_per_transport": 37, + "transports": [ + "subprocess", + "real HTTP" + ], + "http_status_checks": 26, + "log": "public-projection-final.log" + }, + "dependency_proof": "Fresh VGI environment resolved 0.52.3 from PyPI with frozen updated uv.lock; RPC imports from site-packages, no direct_url.json and no PYTHONPATH override. Only the RPC package version changed; Arrow 25.0.1 and DuckDB 1.5.5 remain pinned.", + "wheel_sha256": "c1d71f126901cc5afce9f515fdfa8c9c487faf6003a6a59bbcdec3ba18a997af" + }, + "go": { + "module": "github.com/Query-farm/vgi-rpc-go", + "version": "v0.36.0", + "pull_request": "https://github.com/Query-farm/vgi-rpc-go/pull/7", + "release": "https://github.com/Query-farm/vgi-rpc-go/releases/tag/v0.36.0", + "merge_revision": "42e784e66625fa46c37a0a8f9a1765b969696b43", + "sdk_pin_revision": "4863c12e1d380efb196f9851ffb75ff4c601fccb", + "module_sum": "h1:uzvZxIxATT1r++HC1OnsqShiNEINObF7uGMJX+79LJ0=", + "ci": "All four PR gates passed: minimum Go 1.26, lint, conformance, and full hosted race gate (11m16s). Post-merge CI 40568818497 also passed.", + "artifacts": "qualification/rpc-release-go-20261027-Nm0pra", + "sdk_units": { + "passed": 908, + "skipped": 1, + "packages": 18, + "log": "logs/sdk-unit.jsonl" + }, + "public_projection": { + "cases_per_transport": 27, + "transports": [ + "subprocess", + "real HTTP" + ], + "python_client_rpc": "published PyPI 0.52.3", + "log": "logs/sdk-projection.log" + }, + "dependency_proof": "GOWORK=off, no RPC replacement; proxy.golang.org module download, checksum and release origin 42e784e verified before building/testing.", + "race_qualification_limit": "Parallel ARM64 resource-soak RSS slope exceeded its budget on both unchanged upstream 688ba34 and the candidate; descriptors/threads/children remained flat and no data race was reported. No budget was relaxed or test skipped. Hosted full race gate passed before release, and the extra serial EC2 race run passed 1696 cases. Preserve failing baseline/candidate logs alongside passing runs.", + "sdk_slt": { + "passed_files": 358, + "expected_skips": 32, + "failed": 0, + "selected": 390, + "gate_status": "original coverage floor and skip allowlist passed with all earlier fixture variables restored", + "log": "logs/sdk-slt-full-fixtures.log" + }, + "serial_ec2_race": { + "passed": 1696, + "status": "passed without changed limits or exclusions" + } + }, + "gate_effect": "Closes the Python/Go RPC publication and reproducible dependency blockers. RPC repositories alone were merged/released; VGI preparation branches remain unmerged. Final VGI major version/routing and its coordinated candidate validation remain outstanding; no native DuckDB 2 enablement.", + "diagnostics": "Initial isolated Python checkout lacked sibling SDK paths, causing 19 additional generated-artifact skips; sibling source links restored the final 4661/129 coverage. An initial standalone projection harness lacked VGI_PROJECTION_COMMON=1; final run passed with the intended fixture. No production change was required for either setup correction. Plain Go make test initially passed 350 files/skipped 40; restoring the eight attach-options, schema-reconciliation and versioned-HTTP fixture cases recovered the final 358/32 coverage without changing skip policy.", + "environment_followup": "Owned default Python environment upgraded only vgi-rpc 0.52.0 to 0.52.3. Owned environment.sh now exports GOWORK=off; historical state/go.work is preserved as evidence and no longer selected by default." + }, + "protocol_identity_checkpoint_2026_10_27": { + "status": "implemented_focused_validation_passed", + "protocol_name": "vgi.v3", + "protocol_version": "3.0.0", + "protocol_hash": "50e9404173bee3262e96b8939eb5c425463336abca2a17916812d68b6e0f6cad", + "catalog_contract_hash": "1d12ce672b43ca1510721f89637bb374d27693b100d357ac2ead99fcddc27e9f", + "host": "ec2-user@54.224.166.207", + "artifacts": "qualification/protocol-v3-20261027-Szrx1e", + "scope": "Protocol identity, generated artifacts, current consumers/reflection fixtures and publication namespaces. SDK package versions, independent auxiliary protocols and the unchanged catalog stream digest domain are not coupled to this major. DuckDB 1.5 engine pin is unchanged.", + "gate_effect": "Closes the final protocol identity choice and its focused consistency checks. No VGI merge, public release, final full-language integration matrix or native DuckDB 2 qualification is claimed.", + "validation": { + "generated_artifacts": "All eight repository roots present; strict regeneration check passes with zero drift, including protocol_version.txt in the required generation inventory.", + "python": { + "focused_passed": 350, + "lint": "Ruff check and format pass", + "types": "mypy passes 235 source files", + "log": "python-focused.log" + }, + "go": { + "focused_passed": 38, + "log": "go-rust-csharp/go-focused.jsonl" + }, + "rust": { + "focused_passed": 13, + "log_directory": "go-rust-csharp" + }, + "typescript": { + "passed_per_backend": 88, + "assertions_per_backend": 298, + "backends": [ + "arrow-js", + "flechette" + ], + "declaration_build": "passed", + "log_directory": "ts-java-cpp" + }, + "java": { + "focused_passed": 19, + "failed": 0, + "skipped": 0, + "log": "ts-java-cpp/java-summary.json" + }, + "csharp": { + "focused_passed": 118, + "rpc_dependency": "NuGet QueryFarm.VgiRpc 0.18.1 selected explicitly with VgiRpcCSharpUseSource=false", + "log": "go-rust-csharp/csharp-focused-published.log" + }, + "native": { + "artifact_tests": 6, + "http_test_cases": 8, + "http_assertions": 265, + "subprocess_sql_files": 4, + "http_sql_files": 2, + "cli_reflection": "vgi.v3 / 3.0.0", + "engine_revision": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "engine_version": "v1.5.6-dev149", + "binary_hashes": "native-binary-sha256.txt", + "log_prefix": "native-" + }, + "cpp": { + "focused_cases": 4, + "assertions": 549, + "transports": [ + "stdio", + "Unix socket", + "HTTP" + ], + "worker_rebuilt": true, + "log_directory": "ts-java-cpp" + } + }, + "diagnostics": [ + "The initial Python HTTP rejection assertion omitted the test-client headers argument; corrected the assertion and all 350 focused tests pass.", + "The existing write-finalization probe needed two formatting-only changes for the current Ruff check.", + "The initial C# command selected a historical sibling RPC checkout and failed unrelated IdentityFixture API compilation. The final focused run uses the checked-in published NuGet dependency and passes; both logs are retained." + ], + "review": "Independent read-only cross-repository review found no material issue in active routing, versions, hashes, generated references, publication identities or auxiliary-protocol independence." + }, + "full_candidate_checkpoint_2026_10_27": { + "status": "candidate_suites_passed_published_dependency_release_blockers_remain", + "scope": "Full configured primary SQLLogic lanes and SDK suites; original skip allowlists, file exclusions and execution floors retained. This is not full optional external-service/platform/HTTP SQL coverage.", + "protocol": "vgi.v3/3.0.0", + "host": "ec2-user@54.224.166.207", + "artifacts": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC/qualification/protocol-v3-full-20261027-BwGMpF", + "native_engine_revision": "105edd31b57fe7e6d32ee648efad562e45a9f908", + "native_unittest_sha256": "3d00fad4dbb9d01b4f973368102f0d2cae6a8dd40c729773817f1f6a386ad325", + "sdk_units": { + "python": { + "passed": 4661, + "skipped": 129 + }, + "go": { + "passed": 908, + "skipped": 1, + "race": true, + "coverage": true + }, + "rust": { + "passed": 723, + "ignored": 4, + "explicit_launcher_passed": 3 + }, + "typescript_arrowjs": { + "passed": 743, + "skipped": 68 + }, + "typescript_flechette": { + "passed": 744, + "skipped": 67 + }, + "java_with_patched_rpc": { + "passed": 803, + "skipped": 2 + }, + "csharp": { + "passed": 766, + "skipped": 0 + }, + "cpp": { + "passed": 94, + "skipped": 0 + } + }, + "sql_lanes": { + "python": { + "passed_files": 356, + "expected_skips": 33, + "failed": 0, + "dedicated_crash_passed": 2, + "crash_in_primary": false + }, + "go": { + "passed_files": 358, + "expected_skips": 32, + "failed": 0, + "dedicated_crash_passed": 0, + "crash_in_primary": true + }, + "rust": { + "passed_files": 342, + "expected_skips": 37, + "failed": 0, + "dedicated_crash_passed": 0, + "crash_in_primary": true + }, + "typescript_arrowjs": { + "passed_files": 342, + "expected_skips": 46, + "failed": 0, + "dedicated_crash_passed": 2, + "crash_in_primary": false + }, + "typescript_flechette": { + "passed_files": 342, + "expected_skips": 46, + "failed": 0, + "dedicated_crash_passed": 2, + "crash_in_primary": false + }, + "java_with_patched_rpc": { + "passed_files": 336, + "expected_skips": 37, + "failed": 0, + "dedicated_crash_passed": 2, + "crash_in_primary": false + }, + "csharp": { + "passed_files": 342, + "expected_skips": 37, + "failed": 0, + "dedicated_crash_passed": 2, + "crash_in_primary": false + }, + "cpp": { + "passed_files": 341, + "expected_skips": 38, + "failed": 0, + "dedicated_crash_passed": 0, + "crash_in_primary": true + } + }, + "release_blockers": [ + { + "dependency": "vgi-rpc-java", + "published_version": "0.33.1", + "failure": "16 SDK unit failures plus catalog_contents SQL failure; scalar DICT_INT16_UTF8 record-field annotation is ignored. Patched source passes the complete Java matrix.", + "action": "Land/release minimal dictionary fix, pin the released artifact, and rerun Java without composite source override." + }, + { + "dependency": "native SAB Rust fixture", + "published_vgi_version": "0.35.1", + "failure": "Committed fixture speaks vgi.v2/2.0.0; it cannot interoperate with the v3 native client.", + "action": "Qualify exact v3 candidate with explicit isolated staticlib overrides; update fixture requirements/locks to the matching published Rust SDK during the coordinated release and verify without overrides." + } + ], + "cpp_rpc_publication_proof": { + "qualified_revision": "3ee63c04a32b1bad75d722467ada25760c16ba7d", + "published_version": "0.11.0", + "published_revision": "25fad89ed4bc7746e2958537752a00fe0a707ecd", + "identical_tree": "3e9bd7a46c3fdb78ee2c0e8ba9fea1139aaf4c36" + }, + "test_infrastructure_fixes": [ + "Native unit target links the production extension archive and removes two obsolete boundary stubs to resolve successor helpers without duplicate symbols. Test-only cache paths select candidate SAB staticlibs explicitly.", + "Semantic ALTER diagnostic now verifies ordinary casts, unsupported expression rejection, and generated-column RESTRICT/CASCADE against the already implemented behavior." + ], + "diagnostics": [ + "Initial Python run used too-long AF_UNIX paths; full rerun through a short symlink into the owned workspace passed.", + "Initial Go coverage tooling selected Go1.25.12 under Go1.26 compilation; full race+coverage rerun passed with explicit Go1.26 PATH.", + "Initial native contract selection included two hidden fixture-dependent cases without their fixtures; the corrected 81-case run passes, and both cases pass in their dedicated query/transaction runners." + ], + "tested_revisions": { + "vgi": "7d1597ed2e05a306aaa7ff82878e76d9b00fafbb", + "vgi-python": "4ab6cf0278b24d1288d03701b94ec31f38c00715", + "vgi-go": "ae8f35dc8324c7869648cb2ffd27bf0721397692", + "vgi-rust": "3f46d2272e618c63dfff0929ab0ebdfdebb0a743", + "vgi-typescript": "5ddfd82510db16f3d5a674ddcf8277d922bdfc82", + "vgi-java": "783fda8de9506ef3e98f7d46e0efcfe3d066901b", + "vgi-csharp": "ab8c3bfedd2ff6883b876b23fb8970239aadac25", + "vgi-cpp": "40eea1c662aec4cf679e38e34830ff5f921c6b5b" + }, + "native_references": { + "catalog_contracts": { + "passed_cases": 81, + "assertions": 1864, + "log": "native/catalog-contracts-complete.log" + }, + "macros": { + "passed_cases": 6, + "assertions": 129, + "log": "native/macro-families.log" + }, + "queries": { + "passed_scenarios": 18, + "log": "native/queries.log" + }, + "semantic_alter": { + "passed_scenarios": 6, + "log": "native/semantic-alter-qualified.log" + }, + "transactional_writes": { + "passed_scenarios": 17, + "log": "native/transactional-writes.log" + }, + "runner_tests": { + "passed": 10, + "log": "native/runner-tests.log" + }, + "other_passed": [ + "column paths / required filters / view aliases", + "schema dependencies", + "nullable default schema", + "column metadata and corpus", + "constraint and index reference", + "generated columns", + "projection acceptance", + "repeatability", + "query interruption", + "transaction cleanup", + "HTTP interruption" + ] + }, + "native_support_tests": { + "filter": { + "cases": 14, + "assertions": 233 + }, + "settings": { + "cases": 8, + "assertions": 140 + }, + "cancellation": { + "cases": 9, + "assertions": 37 + } + }, + "native_units": { + "passed_cases": 171, + "assertions": 1395, + "scope": "Unfiltered native unit binary, including SAB ring, C++/Rust RPC and WebWorkerFunctionConnection; candidate Rust staticlibs selected explicitly.", + "log": "native/native-unit-full.log", + "fixtures": "native/candidate-fixtures-proof.json" + }, + "java_minimal_fix": { + "revision": "ec578e142021700be0d471a020d4929da9269e39", + "base_published_revision": "3a8a300a10226be1b56e0458e81ffa3212b73f1d", + "branch": "codex/vgi-v3-record-dictionaries", + "local_worktree": "/Users/rusty/Development/vgi-rpc-java-v3-dictionary", + "scope": "Two files only: scalar record dictionary annotation derivation and regression tests; excludes the unrelated virtual-thread change.", + "rpc_regressions_passed": 5, + "sdk_passed": 803, + "sdk_skipped": 2, + "sql_passed_files": 336, + "sql_expected_skips": 37, + "crash_passed": 2, + "publication": "Committed locally only; not pushed, merged or released." + } + }, + "dependency_fix_checkpoint_2026_10_27": { + "status": "fixes_committed_release_candidates_qualified_publication_pending", + "evidence_root": "/home/ec2-user/vgi-duckdb2-20261013-UgAEHC/qualification/v3-dependency-fixes-20261027-vgttOv", + "java_rpc": { + "candidate_version": "0.33.2", + "fix_revision": "ec578e142021700be0d471a020d4929da9269e39", + "candidate_revision": "2f13ace76882659d1f1bcd745b869bb2099d2bd6", + "core_tests": { + "java21_passed": 680, + "java25_passed": 680 + }, + "ffm_tests": { + "java22_passed": 21, + "java25_passed": 21 + }, + "packaging": "JAR, sources, javadoc and POM built into an isolated Maven repository; SDK resolves the external module with composite source substitution disabled.", + "sdk_unit_passed": 803, + "sdk_unit_skipped": 2, + "publication": "Not pushed, merged or published; SDK dependency pin must remain unmerged until 0.33.2 is available from Maven Central.", + "sdk_revision": "0789d65560eaf4476dcc027e8c60e5af148b45e5", + "sdk_sql_passed_files": 336, + "sdk_sql_expected_skips": 37, + "sdk_crash_passed": 2, + "evidence": "java/summary.json" + }, + "rust_sdk": { + "candidate_version": "0.48.0", + "candidate_revision": "a14e7f9425d16575b29d469aa0cfa24f4fd20497", + "toolchain": "1.97.0", + "passed_release_gates": [ + "cargo fmt --all -- --check", + "cargo clippy -p vgi --all-targets --all-features -- -D warnings", + "cargo build --workspace", + "cargo build -p vgi --all-features", + "cargo test --doc -p vgi --all-features", + "vgi-protocol package dry-run" + ], + "doctests_passed": 9, + "doctests_ignored": 4, + "dependency_changes": "Only the seven workspace package versions change in the ignored local lock; all 493 registry dependency entries remain identical.", + "publication": "Not pushed, tagged or published. Dependent vgi and vgi-client registry package checks require publishing vgi-protocol first.", + "evidence": "rust/summary.json" + }, + "native_fixtures": { + "fix_revision": "8bac1ab254db1133bfe8ac180e875552ffaa6261", + "changes": [ + "Reject linked SDK protocol name/version mismatch before starting SAB worker threads.", + "Require matching registry SDK/protocol, RPC and Arrow dependencies across both fixture locks." + ], + "lock_checker_tests_passed": 6, + "negative_identity_proof": "All five SAB connection tests reject the real registry v2 fixture immediately, without thread abort or hang.", + "candidate_unit_cases_passed": 171, + "candidate_unit_assertions": 1400, + "candidate_fixture_sdk_revision": "3f46d2272e618c63dfff0929ab0ebdfdebb0a743", + "registry_pins": "Original consistent manifests and locks retained. Prepared VGI0.48.0/RPC0.32/Arrow60 changes are an uncommitted qualification patch until the SDK is published.", + "pending_patch": "native/fixture-0.48-publication-pending.patch", + "evidence": "native/summary.json" + }, + "generation": "Strict --check --require-all across all SDKs passes.", + "remaining": [ + "Publish Java RPC 0.33.2 and verify the SDK dependency through Maven Central.", + "Publish vgi-protocol, vgi and vgi-client 0.48.0 in dependency order.", + "Regenerate both native fixture Cargo.lock files from crates.io, commit their matching manifests/locks, and verify the full native unit target without candidate static-library overrides." + ], + "limits": "Local preparation commits only. No VGI main merge, public release or native DuckDB 2 enablement. The prior full language matrix remains the broader coverage checkpoint." + } +} diff --git a/docs/protocol/index.md b/docs/protocol/index.md index 5a85ca66..3c439f5e 100644 --- a/docs/protocol/index.md +++ b/docs/protocol/index.md @@ -11,6 +11,9 @@ Each document declares its own status; “proposed” means the contract is stil | [VGI Secrets](vgi-secrets.md) | **Normative** | How a worker asks for client-side secrets, the two-phase bind, and the layout of the secrets batch. | | [VGI catalog query pushdown](vgi-catalog-query-pushdown-design.md) | **Informative implementation design, proposed** | Updated 2026-10-06: proposed VGI 2.2 query preparation, DuckDB 1.5 explicit execution, and DuckDB 2.0 bound-plan SQL export for automatic pushdown. | | [Proposed VGI protocol changes for DuckDB 2.0](vgi-protocol-proposed-changes.md) | **Informative design/audit report** | Rationale, migration inventory, implementation status, and deferred protocol decisions. It is not an implementation specification. | +| [View security](view-security.md) | **Successor contract, proposed** | Explicit optimization-barrier metadata, read/create profiles, unsupported-reader behavior, and current qualification limits. | +| [Scalar projection expressions](projection-expressions.md) | **Successor contract, proposed** | Optional bind-specific offers, structured expression proposals, partial acceptance, exact appended result fields and local fallback. | +| [Bound scan repeatability](scan-repeatability.md) | **Successor contract, proposed** | Optional per-bind stability claim, snapshot lifetime, conservative defaults and native adapter limits. | ## Reading order diff --git a/docs/protocol/logical-types.md b/docs/protocol/logical-types.md new file mode 100644 index 00000000..bfaa627f --- /dev/null +++ b/docs/protocol/logical-types.md @@ -0,0 +1,140 @@ +# TUPLE and canonical Arrow VARIANT contract + +Status: development contract for the combined successor protocol. VARIANT now +uses the canonical Arrow Parquet Variant representation. This supersedes the +unpublished `vgi.type.variant.arrow.v1` per-value IPC proposal and its custom +scalar encodings. Previous checkpoint test counts describe that earlier source +snapshot; they are not evidence for this replacement. TUPLE's separate VGI +annotation remains unchanged. No DuckDB engine upgrade is part of this work. + +## Negotiation and failure behavior + +The capabilities `vgi.type.tuple.v1` and `vgi.type.variant.parquet.v1` are +independent opt-in entries in `CatalogCapabilities.read`. Selection uses the +existing client/provider intersection. Missing agreement means unsupported. +The superseded `vgi.type.variant.arrow.v1` offer does not select the canonical +profile and has no compatibility alias. + +These requirements apply to nested fields, catalog definitions, query schemas, +parameter batches, and literal defaults. Annotation alone does not negotiate +support. A provider may select a profile only for operations whose semantic +mapping it can implement. Standalone operations need an equivalent explicit +agreement before consuming these values. Unqualified native DuckDB 1.5 adapters +reject them before interpreting Arrow storage, including canonical macro defaults. + +Opaque transport must preserve field and schema metadata, child names/order, +nullability, bytes and validity. Copying a carrier does not authorize executing, +casting or exposing its storage as the logical value. Unknown versions, malformed +storage and unsupported mappings fail; they must not silently become STRUCT, +BLOB, JSON or strings. Query providers decline unsupported schemas before +execution where possible and fail if an unsupported result is discovered later. + +## TUPLE + +`vgi.logical_type.v1 = tuple` is a VGI field annotation, **not** a canonical +Arrow extension. It requires `vgi.type.tuple.v1` and uses struct storage. Ordered +members have transport names `element1` through `elementN`; these labels do not +become SQL member names. The empty tuple is an annotated zero-child struct, +distinct from an empty named struct. Parent validity distinguishes SQL NULL from +a present tuple whose members are all NULL. + +Member types, order, nullability and recursive metadata remain part of schema +identity. An ordinary struct with generated-looking names is still a struct. +Readers must not infer TUPLE from names or shape. Tuple members can themselves +carry canonical VARIANT fields only when both profiles are selected. This says +nothing about representing a DuckDB tuple *inside* Parquet Variant binary data; +that engine mapping remains a separate qualification question. + +The removed draft's `vgi.logical_type.v1 = variant` and special scalar kinds +are rejected. This contract adds no custom binary encoding for HUGEINT, +UHUGEINT, BIGNUM, BIT, TIME_TZ, intervals or geometry. + +## Canonical VARIANT storage + +The Arrow extension is `arrow.parquet.variant`. Its struct contains Parquet +Variant bytes, has no parameters, and uses empty extension metadata. Children +are resolved by their exact, case-sensitive names. The standard permits binary, +large-binary and binary-view storage, with dictionary/run-end encoding also +permitted for `metadata`. +[Arrow canonical extension specification](https://arrow.apache.org/docs/format/CanonicalExtensions.html#parquet-variant) + +VGI's initial `vgi.type.variant.parquet.v1` profile supports the unshredded +subset. Writers use this layout: + +| Item | Representation | +| --- | --- | +| Field metadata | `ARROW:extension:name = arrow.parquet.variant`; `ARROW:extension:metadata =` empty bytes | +| Parent | Arrow struct, with its declared nullability and validity preserved | +| First child | `metadata: binary`, non-nullable | +| Second child | `value: binary`, nullable | + +Readers accept either child order and the canonical binary storage alternatives +above. Exactly the two named fields must be present. Duplicate or extra fields, +including `typed_value`, are rejected by this initial unshredded profile. +Shredding needs a later qualified profile; this restriction does not redefine +the canonical extension. + +A present parent requires non-NULL metadata. The carrier preserves nullable +`value` slots and empty byte strings without asserting that they form a valid +Variant value; that decision belongs to the backend decoder. A NULL parent has +no interpreted value and unused child slots are ignored. Each present cell is +bounded to 8 MiB for the combined metadata and non-NULL value bytes. +Arrow schema traversal is bounded to depth 64 and 4,096 recursive fields. +Enclosing IPC, transport and process memory budgets still apply. + +The binary fields contain **Parquet Variant data**, not an Arrow IPC stream for +each cell. Batch-level Arrow IPC still transports the column. No VGI scalar +envelope, custom scalar encoding or JSON conversion is introduced. + +## Values, validation and engine mappings + +The canonical binary format defines primitives, arrays, objects and null +values. Missing object members and present null members have different binary +representations. A valid parent carrying an encoded root null is also distinct +in transport from an Arrow SQL NULL. These distinctions follow the +[Parquet Variant binary format](https://github.com/apache/parquet-format/blob/master/VariantEncoding.md). +Opaque transport preserves bytes and validity without claiming to decode them. + +The Python helpers construct fields and check negotiation, +recursive schema shape, payload presence and byte limits. It does **not** +implement a general Parquet Variant binary parser or prove arbitrary payloads +valid. The backend decoder must validate binary versions/tags, offsets, +dictionaries, object keys, nested values and supported engine conversions before +using values. Structural checks alone do not qualify a provider for execution. + +This profile preserves Parquet Variant semantics and bytes; it does not promise +preservation of every original DuckDB logical type, width or signedness. Standard +unsigned mappings, including uint8 to int16, uint16 to int32 and uint32 to int64, +are permitted without a custom side channel. An adapter must reject or defer +DuckDB values outside its supported canonical mapping. Unsupported HUGEINT, +UHUGEINT, BIGNUM, interval, temporal, geometry or tuple cases must not revive the +removed scalar encodings. No claim of exact preservation of the full DuckDB type +domain is made. + +The audited Haybarn 2.0 checkout is +`e1784fa4231072d7152b8aaa20977bdb8ff4ace3`: + +- `src/common/arrow/arrow_type_extension.cpp`, `ArrowVariant`, already uses the + canonical extension and currently rejects shredded `typed_value` storage. +- `src/common/types/variant/parquet_variant_encoding.cpp` has incomplete + DuckDB-to-Parquet mappings; each admitted mapping still needs qualification. +- `src/common/types/variant/variant_iterator.cpp`, `RowIsValid`, collapses root + variant-null to SQL NULL. Native roundtrip claims must account for that limit. +- `src/common/arrow/arrow_converter.cpp`, `SetArrowStructFormat`, synthesizes + tuple member names and does not preserve tuple identity by itself. + +## PyArrow transport and remaining qualification + +The PyArrow 25.0.1 integration carries the canonical extension as ordinary +struct/binary storage plus Arrow field metadata. No public PyArrow Variant API +is available in that pin; this work does not register a replacement extension +implementation. IPC round trips retain the canonical metadata and payload +buffers opaquely. Tests distinguish transport from semantic binary decoding. + +`vgi/logical_types.py` owns construction and carrier validation; +`tests/test_logical_types.py` and the shared SDK corpus cover metadata, validity, +child order and malformed storage. Native guard tests cover rejection while +its adapter is unqualified. Complete binary decoding, engine-domain conversion, +native DuckDB import/export, cross-SDK execution and performance remain explicit +implementation gates. Current validation results belong in the manifest's +canonical VARIANT checkpoint, separately from the superseded draft. diff --git a/docs/protocol/macro-contract.md b/docs/protocol/macro-contract.md new file mode 100644 index 00000000..1f123fba --- /dev/null +++ b/docs/protocol/macro-contract.md @@ -0,0 +1,213 @@ +# Macro overload contract + +Status: development contract for the combined successor protocol. The wire +records use one canonical overload-family representation while builds remain +on DuckDB 1.5. Native SQL CREATE MACRO remains unsupported; creation through +the catalog API and native discovery/execution are distinct paths. +The breaking successor uses `vgi.v3` / `3.0.0`; it is incompatible with +released `vgi.v2` implementations. + +The [October 23 combined review](shared-catalog-values.md#combined-protocol-review--october-23-2026) +selects a single overload-family representation for the successor freeze. +Single macros are one-element families. Local authoring helpers can accept +simple definitions, but the wire has no competing legacy definition fields. + +## Representation and capabilities + +`MacroInfo` and `MacroCreateRequest` require nonempty +`overloads: list>`. The existing macro get, list, create and +drop methods operate on the whole named family. There is no new per-overload +RPC or opaque overload identifier. Catalog dependency references likewise name +the complete scalar or table macro family. + +The family shape is mandatory in this successor wire contract; it does not +require the former overload read/create opt-ins. No new capability check is +introduced. Existing dependency, transaction and logical-type requirements +still apply to the relevant operation. A provider implementing creation must +validate, atomically store, and return the complete family. Reading records or +retaining their bytes does not by itself qualify execution. An implementation +that cannot honor a family must reject it, never choose its first overload or +erase typing. + +| Parent field state | Meaning | +| --- | --- | +| One-element overload list | Ordinary single-definition macro. | +| Multiple overloads | Authoritative ordered complete family. | +| Empty overload list | Invalid; dropping a family uses the existing drop operation. | +| NULL or omitted overload list | Invalid. | + +Parent `parameters`, `definition`, `parameter_default_values` and +`arguments_schema` fields have been removed from both records. Function argument +defaults belong to a separate contract and are unchanged. + +The parent `macro_type` and `schema_path` must exactly equal every overload's +kind and raw owner path. Scalar and table definitions cannot share one family. +Replacing a macro replaces the whole family atomically; an overload list is +never an append or patch operation. Ordinary conflict and transaction semantics +remain those of the macro create RPC. + +## Field definitions + +The [combined expression design](shared-catalog-values.md#shared-sql-definitions-and-evaluation-phases) +requires evaluation context on `SqlExpression` and `MacroOverload` for +this same successor release. Both records and all generated SDKs now carry it. +CREATE-time default folding and literal-default discovery remain unchanged; +the new context must not turn defaults into invocation-time expressions. + +`MacroOverload` contains: + +| Field | Encoding and meaning | +| --- | --- | +| `parameters` | Ordered inline `MacroParameter` records, including both required and defaulted parameters. Empty is a valid zero-argument signature. | +| `definition` | Nonempty unbound scalar expression or table query SQL, according to `macro_type`. | +| `profile` | `duckdb.macro.v1`; unknown profiles fail before catalog mutation. | +| `schema_path` | Raw attachment-relative identifier components of the owning schema. A name containing a dot remains one component. No client catalog alias or implicit search-path substitution. | +| `macro_type` | Exactly `scalar` or `table`. | +| `evaluation_context` | Required inline `QueryEvaluationContext`; each supplied default expression must have the same context and raw owner schema as its body. | + +`MacroParameter` contains: + +| Field | Encoding and meaning | +| --- | --- | +| `name` | Nonempty raw parameter name. Names are unique under DuckDB ASCII case folding. | +| `declared_type` | Nullable binary Arrow schema. When present, exactly one nullable field with this parameter's exact name; the physical type and all recursive field/schema metadata define the declaration. The Arrow null placeholder type is invalid here. | +| `default_value` | Nullable binary `TypedValueBatch` IPC. When present, exactly one row and one field with this parameter's name. It is a resolved literal default, including a present typed NULL. | +| `default_expression` | Nullable binary `SqlExpression` IPC. When present, uses `duckdb.macro.default.v1` and the overload's exact owner path. | +| `documentation` | Nullable UTF-8 text. Absent and an explicitly empty string are distinct. It does not alter type identity. | + +`declared_type = NULL` means **untyped**, irrespective of a literal default's +Arrow type. Declaring a type neither supplies a default nor makes the parameter +non-nullable. Both default fields absent means required. A literal typed NULL +is a supplied default, not absence. Both default fields present is invalid. +Required parameters precede defaulted parameters. All values and declarations +retain nested metadata; a receiver that cannot honor a logical type rejects it +instead of falling back to a physical Arrow type. + +The one-row record envelope is exact: missing fields, unexpected schemas, +multiple batches, trailing bytes and NULL in required fields are errors. Limits +are 8 MiB per encoded record, 1 MiB per SQL body or declared type schema, +1,024 parameters per overload, 256 overloads per family, 32 schema components, +4 KiB per identifier and 64 KiB per documentation string. Transport limits may +be smaller. Python serialization revalidates mutable lists before sending. + +## Binding, evaluation and discovery + +The attachment's owning SQL binder is responsible for expression parsing, +name resolution, authorization, logical type resolution, body validation, +implicit casts, duplicate-signature checks and overload selection. It binds +against the specified owner schema and the create operation's transaction. +The profile describes DuckDB macro semantics, not arbitrary SQL execution. +No serialized engine AST, bound expression or cached plan crosses this wire. + +The ordered overload vector must survive create, discovery and copying, but +order is **not** a first-match priority. For `duckdb.macro.v1`, DuckDB considers +provided parameter names, arity and implicit cast costs; typed candidates beat +otherwise equivalent untyped candidates, and tied best matches fail as +ambiguous. Duplicate resolved type vectors are rejected at creation even when +parameter names, bodies or defaults differ. Type aliases must be resolved by +the owner before this check; a generic Arrow codec must not approximate SQL +overload resolution or silently cast arguments. + +For `duckdb.macro.default.v1`, the owner binds defaults as constant expressions +**at creation**. Unsupported or non-foldable expressions fail before +publication. Default values must be compatible with the declared parameter +type according to the engine's binding rules, including integer literal rules; +the request codec deliberately does not substitute Arrow casts for this check. +Defaults that reference parameters are not invocation-time closures. + +Discovery returns **resolved literal defaults** in `default_value`, with +`default_expression = NULL`. It must not return submitted expression text as +the authoritative default after folding. Thus copying a discovered family does +not rerun default expressions under another session or later catalog state. +An owner may retain original SQL in private audit data, but that text is not a +second competing default on the wire. Type declarations, parameter and overload +order, documentation, resolved defaults and owner-relative body semantics must +survive discovery/recreation. SQL whitespace normalization is allowed; changing +binding or default behavior is not. + +The same read representation is required for get, per-schema list and bulk +catalog contents. A family cannot appear as multiple same-name `MacroInfo` +entries. Existing version/ETag and dependency mechanisms identify a change to +any overload as a change to the family. Drop removes the family. + +## Legacy migration and executable reference + +`validate_macro_family` checks the nonempty collection and parent agreement. +Read records additionally reject unresolved expression defaults. The same +representation is used by the public client, provider hook and worker dispatch. + +`normalize_macro_overloads` upgrades a legacy definition without inferring +declared parameter types from `arguments_schema`; callers must supply the +explicit owner evaluation context. That schema's existing types +describe literal defaults; its `vgi_doc` metadata carries documentation. Unknown +or inconsistent legacy metadata is rejected rather than discarded. New +collections are copied through the actual wire codec to isolate stored state +from later caller list mutation. + +`legacy_macro_fields` permits conversion back only when there is one untyped +overload with literal defaults and fully representable documentation/metadata. +The destination evaluation context must match the overload's context. +It rejects multiple overloads, declared parameter types, expression defaults, +and other loss of information. It never evaluates a default or renders an +Arrow value as a SQL string. + +`canonicalize_macro_defaults` accepts the owning binder's resolver, copies the +family, and replaces each supplied default with the resolved literal. It +returns a complete candidate only after every default succeeds. The caller +then publishes the family atomically; it must not publish partial results from +the resolver callback. This helper does not claim to implement a general SQL +binder or catalog transaction store. + +`tests/catalog/test_macro_contract.py` verifies actual Arrow round trips, +recursive type metadata, typed NULL/absence, shape rejection, ownership, +ordering, mandatory family records, safe local migration and failed-default +atomicity. A finite DuckDB 1.5 reference folds a whitelisted expression, stores +and reads the resulting family, copies it without re-evaluation, and compares +macro invocation results. Real DuckDB tests also cover typed overload selection +and duplicate-signature rejection. These establish the contract and bounded +reference behavior; full native catalog adapters and general provider execution +remain separate qualification work. + +`tests/catalog/test_macro_overload_protocol.py` exercises the normal worker and +public HTTP client: canonical create dispatch, get/list/bulk discovery and +rejection of unresolved defaults on discovery. Its finite reference stores +families in the existing SQLite snapshot transaction fixture, covering +reconnect, failed replacement, rollback and commit. This does not claim general +provider transaction or body-binding qualification. + +### Native 1.5 consumer + +Native discovery installs the complete ordered family after validating its wire +records, owner/context, SQL syntax, imported types and resolved signatures. It +never skips a failing member or substitutes the first member for the family. +Defaults pass through the existing Arrow scanner, preserving their actual +DuckDB values and types. The engine selects overloads at invocation. + +Body binding also occurs at invocation. A syntactically valid body with an +unresolved function or relation can therefore be installed and fail when called; +this checkpoint does **not** prove every body semantically valid before native +publication. CREATE-time validation remains the owning provider's responsibility. +Full native body binding and dependency extraction remain separate work. + +The bounded adapter accepts one raw root owner schema and the default qualified +evaluation context, checked again on cached lookups. It qualifies ordinary +function/table references and rejects explicit client catalogs, windows, +CTEs/set operations, unsupported type metadata and unsupported contexts. Native +SQL CREATE MACRO remains unsupported. `vgi_function_arguments()` exposes a +zero-based `overload_index` for macro parameters, their explicit declared types +(`ANY` when untyped), documentation and SQL-literal default displays; function +argument defaults are unchanged. + +The shared corpus includes single-member and multi-member families, typed/untyped +parameters, expression defaults, and ordered overloads. SDK codec checks retain +the inner schema and literal IPC bytes rather than reconstructing logical types +from JSON or display strings. + +The semantics were audited against the pinned Haybarn 2.0 checkout +`e1784fa4231072d7152b8aaa20977bdb8ff4ace3`, specifically +`src/include/duckdb/parser/parsed_data/create_macro_info.hpp`, +`src/include/duckdb/function/macro_function.hpp`, +`src/function/macro_function.cpp`, and +`src/planner/binder/statement/bind_create.cpp`. The binder's default-expression +folding is significant: retaining expression nodes in the parser does not mean +defaults execute afresh on every macro invocation. diff --git a/docs/protocol/projection-expressions.md b/docs/protocol/projection-expressions.md new file mode 100644 index 00000000..2a91975f --- /dev/null +++ b/docs/protocol/projection-expressions.md @@ -0,0 +1,141 @@ +# Optional scalar projection expressions + +This contract is part of the combined, unpublished DuckDB 2.0 protocol +transition, identified as `vgi.v3` / `3.0.0`. It can be implemented and tested +while the native adapter still builds against DuckDB 1.5. The successor is +unpublished and still requires coordinated release qualification. + +## Offer and proposal + +`BindResponse.projection_expression_profiles` is an ordered list of semantic +profiles that the provider is willing to consider for this particular bind. +The default is empty. An empty offer means ordinary scans and local expression +evaluation. Existing `FunctionInfo.projection_pushdown` continues to mean +selection of existing columns; it grants no expression-evaluation support. +Offers contain at most 64 distinct versioned profile identifiers, each at most +256 characters. The initial implementation recognizes +`vgi.expression.duckdb.scalar.v1`. + +The client may call `table_function_projection` after ordinary bind and before +replacing any local expression. This is a pure planning operation: it must not +scan rows, mutate data, or allocate a resource requiring a release call. A +provider may decline every expression even when it offered the profile. + +| Record / field | Meaning | +| --- | --- | +| `TableFunctionProjectionRequest.bind_call` | The original serialized `BindRequest`, including attachment, arguments, owner path, settings, transaction and time-travel context. | +| `bind_result` | The original serialized `BindResponse`, including its exact source schema and opaque provider state. | +| `expressions` | One to 1,024 serialized canonical `SqlExpression` records. Each profile must be offered by this bind. Each owner `schema_path` must equal the bind owner. | +| `result_schema` | An Arrow schema with one requested result field per expression, in the same order. Field names, types, nullability and field metadata are part of the contract. Schema-level metadata must be empty. | +| `TableFunctionProjectionResponse.accepted_indices` | Strictly increasing, unique, zero-based indices into the proposed expressions. The empty list means complete decline. | +| `bind_result` | Null on complete decline; otherwise the complete replacement serialized `BindResponse`. | + +The expression payload reuses `SqlExpression` rather than introducing a second +SQL dialect or an untyped metadata bag. It includes SQL text, a versioned +semantic profile, raw owner path and an explicit `QueryEvaluationContext`. +Filter v2 remains the Boolean predicate contract; its call nodes are not +reinterpreted as scalar result expressions. + +The request requires an ordinary TABLE bind with an explicit owner and no +relational input schema. Source and result field names must be nonempty, +NUL-free and unique under ASCII case-insensitive comparison across both schemas. All proposed +expressions share one evaluation context. Canonical request/response envelopes +are bounded to 8 MiB, including nested records. + +## Result positions and partial acceptance + +An accepted schema preserves every original source field, in order and with +its original metadata. The accepted result fields append in the order of +`accepted_indices`. Schema-level metadata is preserved from the source schema. +There is no source-column renumbering or separate result-mapping object. + +For example, a source schema `[n, label]` and proposed results `[wide_n, +label_size, other]` with `accepted_indices = [0, 2]` produce exactly +`[n, label, wide_n, other]`. DuckDB replaces only the corresponding accepted +expressions with references to slots 2 and 3. It retains `label_size` and its +inputs for local evaluation. Selection/reordering of the resulting scan fields +continues to use the existing `projection_ids` contract. + +The accepted bind result must keep the original profile list and +`repeatability` value unchanged and +must contain no secret-scope request fields. Its `opaque_data` is the complete +replacement provider bind state: the provider must retain any original state +it will need. Existing init, secondary init and HTTP continuation carry that +state and the expanded output schema. No preparation ticket, token registry, +expiration or release method is introduced. + +## Validation and fallback + +The client validates the entire response before changing its plan. Out-of-range, +duplicate or unordered indices, a missing accepted bind result, state returned +on complete decline, changed original fields, wrong appended fields, changed +offers and secret-scope requests are protocol errors. Exact schema validation +includes field metadata and nullability, not only logical type names. + +An unoffered feature or a valid decline leaves the original scan and local +expressions intact. A malformed acceptance or failed RPC is an error. Once +accepted, an execution failure remains an execution failure; the client must +not rerun the scan locally as recovery. This optimization grants no mutation, +retry or repeatability guarantee. + +Moving expression evaluation into a scan can change when an error occurs +relative to a local filter. An adapter must prove equivalence before making a +proposal. In particular, deterministic does not imply safe: narrowing casts, +parsing strings, division and overflowing arithmetic may throw for rows that +a local filter would discard. Unsupported candidates remain local. + +## Initial native adapter boundary + +The first DuckDB 1.5 adapter proof is ordinary catalog table scans and total +`INTEGER` to `BIGINT` casts. It preserves the local projection and accepts +only unambiguous source-column references. Generated-table and direct function-call scans, +nested extraction, throwing casts, arbitrary functions, multi-branch and +partitioned scans require separate qualification. Accepted scans disable +result caching, split enumeration and optimizer hints whose identity or column +coordinates have not been qualified for the expanded schema. + +Bind copies must retain accepted schema/state. A serialized/rebound plan must +either retain the accepted contract or reject the operation explicitly; it +must never silently rebind the original scan while retaining references to +appended result slots. These restrictions are adapter limits, not permission +for a provider to return a different result schema. + +The initial adapter disables prepared-statement bind reuse when this profile +is offered, before optimization starts. Each prepared execution binds and +negotiates again. Restoration of an accepted serialized scan requires the +original bind identity and complete original bind response, including opaque +provider state, to remain identical; otherwise restoration fails explicitly. + +The separate [bound scan repeatability contract](scan-repeatability.md) defines +that promise. Projection acceptance preserves it; accepting an expression does +not itself grant it. + +The Go SDK's RPC dependency unwraps the outer request before the SDK handler +receives it. The published dependency now rejects trailing data, extra batches, +truncated messages and requests exceeding the raw outer 8 MiB limit. It preserves +valid physical-EOF and explicit-EOS stream endings under the +[Arrow streaming format](https://arrow.apache.org/docs/format/Columnar.html#ipc-streaming-format). +The projection parameter type opts into a raw-byte limit before Arrow decoding; +other methods retain their existing size policy. The SDK has a compile-time +dependency on this limit interface, supplied by its published RPC dependency +pin, Go RPC 0.36.0. The dependency tests and all 27 public-RPC cases +pass over subprocess and HTTP. Java, C#, Rust and TypeScript also received +narrow request-local framing fixes. +The Python RPC fix, qualified on October 27, calls the request type's existing +byte decoder rather than unwrapping only its first batch. This restores the +raw 8 MiB limit before decoding; no new size-limit interface is needed. The +default decoder checks the entire single-batch value and retains valid physical +EOF and legacy/modern EOS. Both it and VGI's custom typed-value decoder classify +truncated in-memory bodies as malformed input. Streaming RPC framing is unchanged. + +The public Python runner passes all 27 standard cases and 37 extended cases over +subprocess and real localhost HTTP. The extended cases cover every incomplete +modern/legacy EOS prefix, and HTTP probes verify malformed requests return 400. +Full RPC and VGI Python suites pass. The exact tested RPC commit is `99b3090` on +`codex/single-record-ipc-framing`; see the manifest's +`python_rpc_framing_checkpoint_2026_10_27` for the initial evidence and counts. +The fixes are now merged and published as Python RPC 0.52.3 and Go RPC 0.36.0; +the VGI SDK requirements and locks select those releases. The follow-up +`rpc_publication_checkpoint_2026_10_27` verifies the downloaded packages without +RPC source overrides. These bounded checks do not claim complete transport +conformance or protocol freeze. diff --git a/docs/protocol/scan-repeatability.md b/docs/protocol/scan-repeatability.md new file mode 100644 index 00000000..a3ea19a5 --- /dev/null +++ b/docs/protocol/scan-repeatability.md @@ -0,0 +1,75 @@ +# Bound scan repeatability + +This is a proposed successor contract in the combined, unpublished DuckDB 2.0 +transition. The native implementation continues to target DuckDB 1.5.x. +The bounded native 1.5 and mutable-provider proofs passed on EC2 Linux ARM64. +Broader production-provider qualification and the native 2.0 port remain open. + +## Wire field + +Append `BindResponse.repeatability` after `projection_expression_profiles`. +It is a required, non-null Arrow `dictionary` column. Dictionary +values use the enum names below, exactly and case-sensitively. Missing, null +or unknown values are malformed successor messages. SDK author APIs default +to `UNKNOWN`; opting into a guarantee is optional, with no capability gate. + +| Value | Promise | +| --- | --- | +| `UNKNOWN` | No guarantee that equivalent scans return the same logical rows. | +| `STABLE_WITHIN_QUERY` | Equivalent scans derived from the same complete bound state return the same logical rows and values within one statement. | + +The claim is about that bind's arguments, attachment, transaction/time-travel +context and complete provider state. It does not unify separately bound calls +into a common snapshot. Changing the projected columns or applying an admitted +filter must preserve the corresponding logical result. Ordering and batch +boundaries continue to follow the existing order contract. Sampling, dynamic +filters and other execution modifiers require their own engine-side checks. + +The promise does not authorize retries, supply cross-query cache validity or +imply serializable transactions. Scalar stability, read-only metadata, cache +TTL and catalog version alone cannot establish it. Existing opaque bind state +or an existing transaction snapshot supplies the provider's consistency +anchor; there is no new query identifier, coordinator, token or release RPC. + +## State lifetime + +A provider must retain enough state to honor repeated evaluations without +silently refreshing to newer data. Self-contained snapshot state in +`opaque_data` is one implementation; an existing anchored transaction is +another. If the state expires or becomes unavailable, execution fails. It +must not silently obtain a replacement snapshot under the original promise. + +Bind copies preserve the entire original state. Serialization/restoration must +either preserve it or reject a changed rebind explicitly. A prepared statement +execution obtains fresh bind state for the new statement. Separate executions +may therefore observe newly committed data without weakening either claim. + +Projection acceptance preserves `repeatability` exactly, including `UNKNOWN`. +Its replacement opaque state must retain the original consistency anchor along +with any additional projection state. Accepting an expression cannot upgrade +or downgrade the original promise. + +## Native adapter boundary + +The pinned DuckDB 1.5 adapter decodes and stores the claim, preserves it in +`FunctionData::Copy`, and checks projection replacement. Stable binds disable +prepared-statement bind reuse. Stable inline catalog bind results trigger a +fresh statement bind, and stable scans bypass the result cache because its +current identity omits opaque provider snapshot state. Serialized table-plan +restoration rejects a changed original request or complete bind result. + +DuckDB 1.5 has no `is_repeatable` callback; this change enables no new optimizer +rewrite there. The DuckDB 2.0 port must consult this claim only after its own +sampling/dynamic-filter checks and address the catalog-table shortcut in +`logical_operator_repeatability.cpp`, which classifies a table-backed scan +before consulting the table-function callback. Wiring a callback alone would +not make `UNKNOWN` conservative for VGI catalog tables. + +The mutable SQLite reference in `tests/repeatability_reference.py` captures a +typed snapshot during bind. Its tests mutate committed source data between +scans of one bind, then create a fresh bind. They also cover signed HTTP, +continuation state, stale inline metadata and projection preservation. Native +tests cover copies, changed-state rejection, prepared executions and cache +exclusion. These tests passed on the pinned native 1.5 build. Exact results and remaining +transport/provider limits are recorded in the companion manifest checkpoint +`scan_repeatability_and_go_framing_2026_10_24`. diff --git a/docs/protocol/shared-catalog-values.md b/docs/protocol/shared-catalog-values.md new file mode 100644 index 00000000..6e1952fd --- /dev/null +++ b/docs/protocol/shared-catalog-values.md @@ -0,0 +1,1751 @@ +# Shared catalog values and column paths + +The operation sections through prepared read queries describe implemented +records and bounded reference behavior on the DuckDB 2.0 preparation branch. +The combined-design section distinguishes implemented SQL/constraint records +and reference proofs from the remaining public API and execution work. Both are +part of one combined successor contract. Published main now uses `vgi.v2` / `2.2.0`. The +unpublished successor now uses `vgi.v3` / `3.0.0` for its incompatible changes. +This selects the protocol identity; final coordinated release qualification remains open. The development snapshot +carries typed schema/table options, +layouts, raw column mutation paths, and capability negotiation. See the +operation contracts below. +Completing these codecs does not complete the release gates in the +[combined manifest](duckdb-2.0-manifest.json). + +The [October 23 combined review](#combined-protocol-review--october-23-2026) +is the current scope decision. In particular, the trigger section below is a +retained **deferred design**, not a requirement to add its RPCs to this release. + +## Latest-main integration — October 24, 2026 + +The [integration checkpoint](duckdb-2.0-main-integration-2026-10-24.md) records exact merged +refs, dependency versions and bounded test evidence. Published 2.2's six batched catalog streams +carry the successor records unchanged, including canonical constraints, typed index definitions, +macro families and column metadata. `SchemaInfo` no longer carries attachment state on the wire. +Where selected capabilities project inventory, Python scopes cached responses, publication keys +and etags to that projection. The discriminator is internal; no new RPC or capability is added. + +Identity.v2 and continued table-in-out output are integrated alongside generated native codecs. +Earlier dated version/source/test statements below remain historical. This merge does not freeze +the successor, complete the 2.0 engine port or implement automatic whole-query pushdown. + +## Typed option changes and readback + +Schema, table, and index options use the same literal-value representation. +An option name identifies a column in a `TypedValueBatch`: one Arrow record +batch with exactly one row, retaining the complete schema and field metadata. +Option names are canonical names declared by the provider. The wire layer +performs no case folding, type inference, casts, or SQL expression evaluation. + +`OptionChanges` has these fields, in order: + +| Field | Arrow type | Meaning | +| --- | --- | --- | +| `set_values` | nullable `binary` | IPC stream containing a `TypedValueBatch` of the options to SET. NULL means no SET operations. | +| `reset_names` | non-null `list` | Names whose stored overrides are removed. Empty means no RESET operations. | + +The inner batch must have unique, nonempty, non-NUL field names and exactly one +row. More than one IPC batch, a missing batch, and trailing bytes are invalid. +An empty value set still has one row and zero columns. Types, nullability, and +metadata are validated against the provider's declared option schema, including +nested field metadata. Each supplied field must match its declaration exactly; +the selected fields can appear in any order. Schema metadata must also match. + +| Request | Result | +| --- | --- | +| A field contains a non-NULL literal | Store that typed value as an explicit override. | +| A field contains a typed NULL | Store an explicit NULL override, if the declared type permits NULL. | +| A name appears in `reset_names` | Remove its override and restore the provider default resolved for this mutation. | +| A name appears nowhere | Leave its value and override status unchanged. | +| A name appears in SET and RESET, RESET repeats a name, or the name is unknown | Reject the entire change before mutation. | + +`OptionState` provides readback: + +| Field | Arrow type | Meaning | +| --- | --- | --- | +| `values` | non-null `binary` | IPC stream containing the effective value of every exposed option. | +| `explicit_names` | non-null `list` | Names with stored overrides, including explicit NULL and values equal to the default. | + +Every explicit name must exist in `values`, with no duplicates. Readback is a +snapshot: it must be produced using the same visibility rules as the owning +catalog object. RESET uses defaults from the mutation's transaction/snapshot, +not defaults remembered by the client at attachment time. Resetting a known +option without an override is valid. Provider policy can restrict which known +options are mutable. + +`OptionChanges.apply_to(current, defaults=..., mutable_names=...)` is the Python +reference implementation. It validates the entire candidate and returns a new +`OptionState`; it performs no persistence. A provider must validate additional +range, dependency, authorization, and operation-specific constraints before +committing it. The helper alone does not establish durable atomicity, DDL +rollback, or replay safety. A SQL expression string is a string literal here; +expression-valued options require an explicitly supported expression contract. + +```python +import pyarrow as pa +from vgi.catalog.options import OptionChanges, OptionState +from vgi.typed_values import TypedValueBatch + +schema = pa.schema([pa.field("label", pa.string())]) +defaults = TypedValueBatch(pa.RecordBatch.from_pylist([{"label": "default"}], schema=schema)) +current = OptionState(values=defaults) +null_value = TypedValueBatch(pa.RecordBatch.from_pylist([{"label": None}], schema=schema)) +changed = OptionChanges(set_values=null_value).apply_to(current, defaults=defaults) +assert changed.explicit_names == ["label"] +assert changed.values.batch.column("label")[0].as_py() is None +reset = OptionChanges(reset_names=["label"]).apply_to(changed, defaults=defaults) +assert reset.explicit_names == [] +assert reset.values.batch.column("label")[0].as_py() == "default" +``` + +Schema option definitions, RPC integration and a transactional reference are +implemented below. Broader option-bearing objects, dependency effects and +shipping adapter qualification remain required before the combined freeze. + +## Raw column paths + +`ColumnPath` is an ordered list of nonempty, non-NUL strings. The first component +is a top-level column, and subsequent components identify nested fields. Dots, +quotes, reserved words, and Unicode characters are literal components. There is +no dotted SQL parsing or case conversion on the wire. An engine resolves SQL +identifiers against catalog metadata and sends the canonical field spelling. + +`column_path_key` returns the exact component tuple for identity comparisons. +`resolve_column_path` checks exact names against an Arrow schema and descends +through struct fields. Missing or ambiguous names and non-struct traversal +reject. List-element, map-key, and union-arm traversal is not implicit. +Engine-specific identifier equivalence remains an adapter responsibility. + +`RequiredFilterPaths.groups` is a non-null +`list>>`: AND groups, OR alternatives within each group, then +the raw components of each alternative. An empty outer list imposes no +requirements; empty groups and empty paths reject. Duplicate alternatives in +a group reject. Every alternative must resolve against the table schema. + +```python +from vgi.column_path import RequiredFilterPaths + +required = RequiredFilterPaths(groups=[[["a", "b"], ["other"]], [["a.b"]]]) +assert required.is_satisfied_by([["a"], ["a.b"]]) +assert not required.is_satisfied_by([["a.b"]]) +``` + +Each group requires at least one alternative to have a present filter path as +a component prefix. A filter on `['a']` satisfies `['a', 'b']`; a filter on +`['a.b']` does not. A child filter does not satisfy a requirement for its parent. +This preserves the existing presence-policy semantics. It does not prove +predicate selectivity, authorization, or remote enforcement. The adapter must +still enforce the independently negotiated filter semantics. + +`RequiredFilterPaths.from_legacy(groups, schema=...)` is an explicit migration +helper for the formerly dotted `TableInfo.required_filters`. It enumerates +schema paths and accepts a legacy spelling only if it identifies exactly one +path. When both a literal column `a.b` and struct field `a.b` exist, migration +fails and requires the catalog author to choose the intended components. It is +never an automatic fallback when decoding the successor representation. + +The successor `TableInfo.required_filters` carries these groups directly as +non-null `list>>`: AND groups, OR alternatives, raw components. +An explicit empty outer list means no requirements. Missing or NULL metadata, +legacy two-level lists, NULL/empty groups or paths, NULL/empty/NUL components, +duplicate alternatives, and paths missing or ambiguous in the complete table +schema are invalid. A traversal must pass through struct fields; list/map/union +traversal is not implicit. The pinned native 1.5 parser rejects invalid metadata +before scanning instead of silently substituting an empty requirement. Its +`vgi_required_filters` table tag contains the same three-level JSON structure. + +The native optimizer retains raw components through table filters and bound +struct-extract chains, using the engine's resolved field indexes for canonical +names. A deep child reference does not additionally register a whole-parent +filter. Diagnostics quote individual components when needed so literal dots and +quotes remain distinguishable from traversal. This is an unpublished breaking +change in the same successor release; it has no automatic legacy wire fallback. +`TableInfo` is nested IPC inside a binary RPC result, so its schema change does +not by itself change the RPC reflection hash. The generated schema inventory, +shared IPC corpus, and coordinated major-version transition remain required. + +The unpublished successor uses `column_path: list` in the nine existing +column mutation RPCs: add, drop, rename, type change, default set/drop, +nullability set/drop, and comment set. ADD and type change also carry an explicit +path before `column_definition`. That IPC schema must contain exactly one field, +whose name equals the last path component, with no schema metadata or trailing +payload. A type-change definition carries the new type and its logical-type +annotations; column defaults, comments, tags, compression, and nullability must +be changed through their own contracts rather than silently discarded. + +`["a.b"]` identifies one literal top-level column; `["a", "b"]` identifies a +field in a struct. A rename's `new_column_name` is one raw component and leaves +the owning path and field position unchanged. ADD appends within its immediate +parent. Nested edits preserve parent metadata and NULL parent structs; they do +not materialize a non-NULL struct merely to hold a new defaulted field. The +reference rejects removing the last field of a table or struct, invalid paths, +failed casts, invalid defaults, and nullability violations before publishing any +metadata, data, or version change. Metadata-only edits preserve existing +physical partitions, including after a future-writes-only layout change. + +Each nested operation requires its selected attachment profile: +`vgi.column.nested.{add,drop,rename,type,default,nullability,comment}.v1`. +The worker rechecks current provider support before dispatch. Transactional +nested DDL also requires `vgi.transaction.ddl.v1`. One-component paths keep +baseline flat DDL behavior; the pinned 1.5 adapter emits one component without +splitting dots. Flat-only SDK providers reject deeper paths. The CLI continues +to accept a literal column name and encodes it as one component. + +These changed parameter types and required targets make the current combined +diff breaking; retaining them requires a major version and corresponding +routing transition. Existing 2.1 deployments retain their published contract. +No duplicate field-add/drop/rename RPC family is needed. DEFAULT/USING now use +the mandatory canonical payload described in the October 23 implementation +checkpoint. Generated dependencies, full column metadata and complete +cross-object CASCADE remain part of the combined contract work. The limited +reference does not qualify those remaining semantics or every logical type. + +## Cross-SDK evidence + +`SHARED_CONTRACT_TYPES` in `vgi/codegen/_common.py` drives schema generation and +the Go, Rust, TypeScript, Java, and C# typed-model inventories. Java's generator +supports recursive lists using its existing RPC codec, validating nullable +`item` fields at every list level. C++ receives schema factories. + +`vgi.codegen.catalog_contract_fixtures` generates +`conformance/catalog-contracts.json` into every worker SDK. Its populated and +empty records exercise typed NULL payloads, schema and nested field metadata, +explicit overrides, literal dotted names, quotes, and three list levels. Each +SDK decodes Python-written IPC through its real codec and encodes it again; +Go exercises its codec through an HTTP echo service. TypeScript checks both +Arrow implementations. The tests compare opaque value payloads byte-for-byte; +they do not claim those SDKs can interpret every logical-type annotation yet. + +The corpus also contains 18 column mutation requests: all nine operations with +both literal dotted targets and nested paths. SDK tests preserve the definition, +transaction, nullable comment/expression, rename component, and boolean flags. +TypeScript's public client-to-handler checks also use the raw RPC list-scalar +representation, which can be an Arrow vector rather than a JavaScript array. + +Regenerate and require every checkout with +`scripts/regen_generated.py --require-all`, then check drift with +`scripts/regen_generated.py --check --require-all`. The protocol release still +requires the complete manifest, semantic implementations, all operation and +unsupported-path tests, and engine/transport qualification. + +## Attachment negotiation and schema-option operations + +The development contract now carries an optional `CatalogCapabilities` record +inside `ClientCapabilities.catalog`. Its four non-null string lists are `read`, +`ddl`, `write`, and `transaction`. Each name is a case-sensitive, explicitly +versioned profile. Missing offers select no new profiles. Unknown offered +profiles/versions are ignored, never downgraded to another version. + +`CatalogAttachResult.catalog_capabilities` carries the provider's advertisement; +`selected_capabilities` carries the worker's intersection of the offer, current +provider support, and profiles understood by the worker. Selection is sorted +and excludes profiles whose prerequisites were not selected. Clients must use +the selected profiles, not infer permission from the advertisement or the RPC +method inventory. Capabilities do not replace authorization. + +The initial implemented profiles are: + +| Category | Profile | Contract | +| --- | --- | --- | +| read | `vgi.schema.options.read.v1` | Discover typed definitions and read effective values and explicit overrides. | +| ddl | `vgi.schema.options.create.v1` | Supply literal schema creation options; requires the read profile. | +| ddl | `vgi.schema.options.alter.v1` | Atomically SET/RESET schema options; requires the read profile. | +| transaction | `vgi.transaction.ddl.v1` | Advertised DDL participates in begin/commit/rollback and transaction-local reads. Does not imply mixed DDL/DML or cross-catalog atomicity. | + +Python seals the selection inside the attachment alongside its UUID and +provider bytes. MetaWorker routing and authenticated HTTP reconnects retain it; +providers and storage still see their original bytes/UUID. Sealed attachments +remain bound to the principal. Local transports without sealing have the same +trust boundary as their existing opaque attachment values. Selected profiles +are immutable for an attachment: changing the offer requires a new attachment. +A provider downgrade is checked at each new operation. A missing/stale profile +raises `UNIMPLEMENTED` / `catalog_capability_required` before calling the +provider's mutation hook. Transactional option mutations additionally require +the DDL transaction profile. Providers must validate any changing object-level +requirements atomically with their effects; this profile check does not yet +implement trigger or secure-view requirements. + +Schema options are omitted from metadata returned to attachments without the +read profile. Capability-dependent catalog snapshots bypass the shared +attachment-independent cache, and their etags hash the effective agreement and +projected contents. An etag from a different capability projection cannot +suppress the required readback. The framework's existing authentication check +still runs before cache access. + +`OptionDefinitions` contains `schema` (serialized Arrow schema), `defaults` +(`TypedValueBatch` IPC containing a subset of the declared fields), and explicit +`create_names` and `alter_names` allowlists. Names without defaults are required +at creation, including nullable options, and cannot be RESET. Read-only options +must have provider defaults. Empty allowlists grant no mutation rights. Every +field and all metadata in the defaults and caller values must match the declared +schema. Helpers `create()` and `apply()` construct a complete candidate before +any provider persistence. Defaults are resolved again in the mutation's +transaction, so discovery does not reserve a default value. + +| Method/record | Development change | +| --- | --- | +| `catalog_schema_option_definitions` | Attach, raw schema path, optional transaction; returns `OptionDefinitions` for an existing or proposed schema path. Requires the read profile. | +| `catalog_schema_create` | Adds nullable `options` binary containing `TypedValueBatch` IPC. Non-NULL requires the create profile, including an explicitly empty batch. NULL retains baseline creation behavior. | +| `catalog_schema_alter_options` | Attach, raw schema path, non-null `changes` binary containing `OptionChanges` IPC, optional transaction; returns no result. Requires the alter profile. | +| `SchemaInfo.options` | Nullable binary containing `OptionState` IPC. Read back through get, list, or full catalog contents. NULL means unavailable, not an empty option set. | + +Flat RPC arguments carry IPC bytes explicitly; Python's public client accepts +the typed helper objects and serializes them. The provider hooks receive typed +objects. Readback and catalog versions follow the existing transaction visibility +rules. A lost mutation/commit response does not grant permission to replay it. +The wire profile does not invent automatic retries or exactly-once execution. + +`tests/catalog/schema_options_reference.py` supplies a durable SQLite reference +with persisted transaction snapshots and conflict detection. Public HTTP tests +cover typed NULL, RESET, isolation, rollback, commit on a new worker, and +idempotent completion of that reference's transaction identifiers. Other tests +cover unknown/absent offers, provider downgrade, mandatory transaction profiles, +MetaWorker routing, malformed/tampered tokens, cache projection, and atomic +rejection of a mixed valid/invalid change. This evidence qualifies the Python +reference behavior; it is not native DuckDB 2.0 qualification. + +Other SDKs carry the new schemas/codecs and retain conservative absent +agreements. Their current mutation handlers must reject a non-NULL options +payload before handing an unsupported operation to a provider. Native DuckDB +1.5 offers no schema-options profiles. The combined public protocol +version remains undecided until the remaining contract changes are reviewed; +this development wire snapshot must not be published as a stable 2.1 release. +Schema comment/tag alterations are implemented in the later checkpoint below; +current operation-specific requirements and remaining work follow the October 23 +review. No generic object-requirements carrier is selected. + +## Table options, creation metadata, and layout + +The unpublished table increment adds nullable `comment`, `tags`, `options`, and +`layout` fields at the end of `TableCreateRequest`. NULL means no new request; +an empty map, empty typed batch, or empty layout is still an explicit request +and requires its selected capability. Existing conflict and constraint fields +retain their meanings. A provider must validate all supplied metadata before +creating or replacing a table. + +`TableInfo` appends nullable `options` (`OptionState` IPC) and `layout` +(`TableLayout` IPC). Get, schema listing, and bulk catalog contents project these +fields according to the sealed attachment agreement. Layout metadata is omitted +when any expression profile it requires is unavailable. Projection contributes +to the response etag, so clients with different profiles cannot share an +incompatible cached representation. Layout describes storage policy; it never +promises the order returned by a scan. + +| Operation | Required selected profile | +| --- | --- | +| Option definitions and option readback | `vgi.table.options.read.v1` | +| Explicit creation options | `vgi.table.options.create.v1` | +| Table option SET/RESET | `vgi.table.options.alter.v1` | +| Layout readback | `vgi.table.layout.read.v1` | +| Explicit creation layout | `vgi.table.layout.create.v1` | +| Change layout for subsequent writes | `vgi.table.layout.alter_future.v1` | +| Rewrite existing data to a replacement layout | `vgi.table.layout.alter_rewrite.v1` | +| Explicit creation comment or tags | `vgi.table.metadata.create.v1` | + +Option and layout mutation profiles require their respective read profiles. +New operations inside a transaction additionally require the selected DDL +transaction profile. Current provider support is checked again before effects. +Absent capability means rejection, including when the request is empty. + +The new flat RPCs are `catalog_table_option_definitions`, +`catalog_table_alter_options`, and `catalog_table_alter_layout`. All target an +attachment, raw `schema_path`, table `name`, and optional transaction handle. +Definitions may describe an existing or proposed table. Option alteration takes +`OptionChanges` IPC; layout alteration takes `TableLayoutChange` IPC. Creation +options are a `TypedValueBatch`, using the same validation and RESET rules as +schema options. The Python client accepts typed objects and serializes these +flat binary parameters explicitly. + +### Expression and layout records + +`SqlExpression` carries nonempty `sql`, a versioned semantic `profile`, and a raw, +attachment-relative owner `schema_path`, and required `evaluation_context: +QueryEvaluationContext`. The receiving provider binds the SQL +against the target table. No bound expression, private AST, caller catalog alias, +or session search path crosses the wire. The owner must resolve to the target's +schema. Profiles must define grammar, functions, types, NULLs, determinism, and +errors before publication; accepting arbitrary SQL is not implied. + +`SortKey` contains `expression` (a `SqlExpression` IPC stream), explicit +`direction`, and explicit `null_order`. Session defaults must be resolved by the +caller. On the Arrow wire, enum dictionaries carry member **names**: +`ASC`/`DESC` and `FIRST`/`LAST`, not Python's lowercase enum values. + +`TableLayout` contains ordered, non-null `partition_by` and `sorted_by` lists of +binary IPC records (`SqlExpression` and `SortKey` respectively), plus `coverage`. +Empty lists mean no corresponding policy. `coverage` is `ALL_ROWS` or +`FUTURE_WRITES`; new empty tables require `ALL_ROWS`. + +`TableLayoutChange` has a mandatory `mode` (`FUTURE_WRITES` or `REWRITE`) and +nullable replacement lists `partition_by` and `sorted_by`. NULL leaves that +component unchanged; an empty list clears it. Both NULL is invalid. The complete +candidate includes retained keys: their profiles must remain selected and +currently supported too. Providers revalidate the entire candidate under the +mutation's snapshot to handle concurrent changes. + +`FUTURE_WRITES` leaves existing data untouched and sets coverage accordingly. +`REWRITE` succeeds only after existing data conforms to the complete replacement +layout and sets coverage to `ALL_ROWS`. Validation, expression evaluation, or +rewrite failure must leave metadata, data, and the visible catalog version +unchanged. A provider unable to perform the requested mode rejects it; neither +client nor server may silently change modes or retry a mutation automatically. +This contract does not provide mixed DDL/DML or cross-catalog atomicity. + +### Reference behavior and qualification limits + +`tests/catalog/table_layout_reference.py` persists schema/table metadata and +physical partition groups in SQLite transaction snapshots. Its pinned DuckDB +1.5 evaluator binds expressions and actually partitions and sorts fixture rows. +Tests exercise durable readback, nested owner identity, option RESET and typed +NULL, future-write preservation, rewrite commit/rollback, capability withdrawal, +and atomic rejection of a failing rewrite. Fixture row seeding is not a public +VGI data-write capability. + +The candidate `vgi.expression.duckdb.scalar.v1` reference currently accepts only +column references, constants, `+`, `-`, `*`, `%`, `abs`, `lower`, `upper`, and +`coalesce`, subject to DuckDB binding and type checks. It rejects subqueries, +extra statements, qualified/custom functions, and other expression classes. +The parser is used locally for validation only. Existing tests establish selected +integer/NULL cases, not the complete logical-type or DuckDB 2.0 semantic matrix; +that profile remains subject to the combined P1 review. + +All SDKs receive the records and shared Python-written IPC corpus, including +NULL versus empty lists, explicit ordering, nested binary values, and creation +metadata. SDK workers without these profiles reject new creation payloads before +mutation and register unsupported RPCs with structured UNIMPLEMENTED errors. +The native 1.5 adapter sends NULL for the four new creation fields and continues +to advertise no table-options/layout profiles. SQL enablement, production provider +implementations, and the native 2.0 adapter remain separate unfinished work. + +## Schema creation metadata + +`catalog_schema_create` already carries nullable `comment`, nullable string-map +`tags`, and `on_conflict`. The successor preparation reuses these fields; it +adds no method or field for creation metadata. A NULL comment means absence and +an empty string is a present comment. NULL/omitted tags create an empty map. +Tag keys and values are literal strings, including empty values and dotted keys. +Get, list, and whole-catalog snapshots preserve this metadata; optional +`estimated_object_count` hints may differ between discovery routes. + +ERROR leaves an existing schema and its catalog version unchanged. IGNORE also +preserves its complete metadata and version. For replacement of an empty schema, +REPLACE installs the supplied metadata and clears omitted comments/tags. New +schema creation and replacement advance the mutable catalog version. This +checkpoint does not settle replacement of nonempty schemas, descendant schemas, +or cross-object dependencies; their combined lifecycle contract remains open. + +All seven bundled mutable `contents_reval` providers run the same public-client +creation/readback/conflict test. It distinguishes a literal dotted component +from a nested path. The durable Python schema-options reference additionally +qualifies metadata together with typed options across HTTP reconnect, committed +replacement, option alteration and rollback. The older Python transactor rejects +schema metadata and REPLACE before executing DDL; Go's older writable provider +rejects unsupported tags and empty comments. These references do not establish +transaction guarantees for every provider. + +The Go wire `SchemaInfo.Comment` uses `*string`; static author-facing schema +comment descriptors keep their historical empty-string default. TypeScript's +provider `schemaCreate` receives the existing conflict policy as a final optional +argument, and decodes the Arrow tag map before delegation. + +Native DuckDB 1.5 forwards both CreateSchemaInfo metadata fields and distinguishes +NULL from empty comments during either cache-loading path. Because that engine +rejects `COMMENT ON SCHEMA`, the native integration test creates metadata through +DuckDB's parsed CREATE API, then uses SQL database copy and catalog discovery. +Build with `-DBUILD_VGI_CATALOG_TESTS=ON`, set `VGI_TEST_WORKER` to a worker location, +and run `build/release/extension/vgi/vgi_catalog_tests '[catalog-metadata]'`. +The public-client tests are in `tests/catalog/test_view_metadata_cross_sdk.py` +(set `VGI_METADATA_WORKERS` to SDK-name/worker-argv JSON); durable HTTP coverage is +in `tests/catalog/test_schema_metadata_protocol.py`. + +At this creation checkpoint, schema metadata mutation was still open; the +following section records its subsequent contract and reference implementation. +Native nested-schema routing and native 2.0 qualification remain open. Neither +checkpoint freezes or releases the protocol. + +## Atomic schema metadata mutation + +`catalog_schema_alter_metadata(attach_opaque_data, path, changes, +ignore_not_found=false, transaction_opaque_data=NULL)` applies one +`CatalogMetadataChanges` IPC record. The change record is shared so future +object-specific metadata operations can use identical semantics. Its fields are: + +| Field | Required wire representation | Meaning | +| --- | --- | --- | +| `set_comment` | non-null bool | False leaves the comment unchanged; true applies `comment`. | +| `comment` | nullable UTF-8 | With `set_comment=true`, NULL clears and `""` stores an empty comment. A non-null value with `set_comment=false` is invalid. | +| `set_tags` | non-null map of UTF-8 to UTF-8 | Set the supplied literal, case-sensitive keys. Empty values and empty keys are valid; map order has no meaning. | +| `remove_tags` | non-null list of UTF-8 | Remove exactly these keys. A missing key is a no-op. Duplicate keys are invalid. | + +The IPC payload has exactly one batch, one row and all four fields; additional +batches and trailing bytes reject. Missing fields, wrong types, +NULL non-nullable fields, duplicate SET keys, duplicate removals, and overlap +between SET and REMOVE reject before the provider is called. Tags are metadata +strings, not parsed SQL identifiers; a dotted key remains one key. `path` uses +nonempty raw schema identifier components without NUL. Invalid requests reject +even with `ignore_not_found=true`. + +The provider validates the target, authorization and any object restrictions, +then applies the whole patch atomically in the transaction's snapshot. It must +preserve options, child schemas and contained objects. A missing target rejects +unless explicitly ignored; an empty patch does not bypass this existence check. +If the effective metadata is unchanged, versions and etags stay unchanged. +Changes advance the transaction-visible catalog version. Committed changes must +invalidate old catalog snapshots; rollback must preserve committed metadata and +versions. There is no implicit retry after an ambiguous transport failure. + +The operation requires selected DDL profile **`vgi.schema.metadata.alter.v1`**. +It needs no additional read profile because schema comments and tags already +have mandatory readback fields. A transaction handle also requires +`vgi.transaction.ddl.v1`. The Python worker checks the immutable attachment +agreement against current provider support before applying the operation; +withdrawn support, unnegotiated support and a transaction owned by another +attachment reject before effects. Negotiation does not grant authorization. + +The durable SQLite schema reference implements this profile through the public +HTTP client. It covers readback, literal/nested path isolation, options +preservation, simultaneous comment/tag edits, no-ops, missing targets, malformed +patches, commit, rollback, reconnect, etag invalidation and routed sealed handles. +The seven ordinary SDK fixture catalogs do **not** advertise this new profile; +the public-client test requires their calls to reject with no metadata/version +change. Their generated request inventories and shared codec corpus include the +new method/record. This is P1 reference behavior plus P2 schema preparation, +not qualification of production metadata mutation in every SDK. + +`test_metadata_changes.py` checks strict shape and patch semantics. All SDK codec +suites round-trip populated, clear-comment and no-op vectors, including a literal +`__proto__` tag. This exposed and fixed TypeScript's generic map normalizer losing +that key; Arrow-js and Flechette codecs are checked separately. Flechette's +previously recorded stdio transport limitation remains open. + +The pinned DuckDB 1.5 engine and refreshed Haybarn 2.0 source both reject SQL +`COMMENT ON SCHEMA` (2.0: `src/parser/peg/transformer/transform_comment.cpp`). +Native registration of this RPC does not enable that grammar or advertise the +new profile. Native SQL wiring, full SDK capability/provider implementations, +nonempty schema replacement/drop dependencies, and the coordinated major +version freeze/release remain open. + +## Dependency authority and schema lifecycle + +`catalog_dependencies(attach_opaque_data, transaction_opaque_data=NULL)` returns +one `DependencySnapshot` under the selected read profile +`vgi.catalog.dependencies.read.v1`. The provider constructs the complete graph +and its `catalog_version` from one transaction-visible snapshot. A graph is +advisory: a later mutation must resolve and authorize its target and every +cascade member against the authoritative provider state inside the mutation's +transaction. Discovery is neither a reservation nor permission to delete. +When policy prevents complete disclosure, reject this read rather than omit +inaccessible objects or edges. Enforcement still includes those objects. + +`CatalogObjectRef` has four fields: + +| Field | Meaning | +| --- | --- | +| `kind: string` | `schema`, `table`, `view`, `index`, `scalar_macro`, `table_macro`, `sequence`, `type`, `scalar_function`, `aggregate_function`, `table_function`, `window_function`, or `trigger`. Unknown kinds reject. A kind does not advertise DDL support. | +| `schema_path: list` | Raw owning-schema components. For a schema object, these are its parents; an empty list is valid only for a root schema. | +| `name: string` | The final raw object identifier. Function/macro references name the overload family, not a private engine overload handle. | +| `owner_table: string?` | Required only for a trigger, whose name is unique within its owning table. NULL for all other kinds. | + +All identifier components are nonempty strings without NUL. The attachment +supplies catalog identity; references contain no client aliases, pointers or +engine OIDs. Providers resolve input according to their identifier policy and +return canonical spelling consistently throughout the graph. The shared record +preserves spelling, case and component boundaries; it does not split dotted +names or silently fold Unicode. Cross-attachment dependencies are outside this +profile: a provider cannot advertise this complete graph while silently omitting +an external dependency. This does not promise distributed catalog transactions. + +`ObjectDependency` carries `dependent` and `subject` as complete +`CatalogObjectRef` IPC records, plus `kind: string`: + +- `requires`: dropping the subject is blocked under RESTRICT while a dependent + would remain. CASCADE includes the dependent. Schema membership and nested + schemas' parent edges use this policy. +- `owned`: the dependent is automatically included when its owner is dropped, + including under RESTRICT. It may also be dropped explicitly. Dropping a + dependent never drops its subject. An object has at most one owner, and + ownership cycles reject. Indexes/triggers may use this policy only where the + provider implements its full semantics. + +`DependencySnapshot` contains a nonnegative int64 `catalog_version`, +`objects: list` and `dependencies: list`. Objects with no edges +are included. All IPC records contain exactly one complete row and one batch; +missing fields, incorrect types/nullability, trailing bytes, duplicate object +identities, dangling endpoints, self edges, duplicate/conflicting edges and +invalid owner graphs reject. List order does not affect graph meaning. Reference +cycles are permitted; CASCADE computes their finite deletion closure. + +The drop planner first computes the full closure of owned objects (and all +references for CASCADE), then checks for blocking edges. This prevents an edge's +iteration order from changing RESTRICT behavior. It returns a deletion set, not +an execution order: providers must apply it atomically, authorize all members, +and preserve graph consistency on failure. A provider may impose additional +object-specific restrictions, such as rejecting an internal/default schema. + +The optional DDL profile `vgi.schema.lifecycle.v1` qualifies the existing +`catalog_schema_create` and `catalog_schema_drop` RPCs with these guarantees: + +- Create resolves the complete raw target and requires existing parents. ERROR + and IGNORE do not modify existing metadata. REPLACE is an atomic RESTRICT drop + and creation; it never implicitly cascades, retains descendants, or orphans + objects. Omitted metadata/options take the new schema's defaults. +- DROP with `cascade=false` rejects a nonempty schema. `cascade=true` removes its + subtree and dependent objects, including dependents in other schemas of the + same attachment. Missing targets obey `ignore_not_found`; ignoring a missing + target changes neither catalog version nor etag. +- Failed validation/authorization/dependency checks leave all objects, metadata + and versions unchanged. Successful changes advance the transaction-visible + version once and invalidate committed snapshots only after commit. Rollback + restores the previous committed graph. A concurrent committed writer cannot + be overwritten by an older transaction's metadata snapshot. +- Selecting this guarantee requires current provider support at each schema + mutation. Transaction handles additionally require `vgi.transaction.ddl.v1`. + Legacy availability of these existing RPCs is retained without claiming this + profile. Regardless of negotiation, providers must not bypass authorization or + their dependency enforcement. + +At the inspected Haybarn 2 pin, `CreateSchemaInfo::ShouldReplaceOnConflict`, +`DuckCatalog::CreateSchema` and `test/sql/catalog/nested_schema/drop_nested_schema.test` +confirm REPLACE uses a non-cascading drop and nested children block it. No new +REPLACE/CASCADE flag is needed on the schema-create wire request. + +The durable SQLite Python reference now implements schema, table and simple-view +dependencies over the public HTTP client, sealed MetaWorker routing and persistent +transactions. It binds only `SELECT * FROM` one table/view (owner-relative or one +explicit schema component); other SQL rejects before creation. It does not +execute view queries or implement table data writes. Ownership/drop-closure +semantics have executable shared-record tests; production index/trigger/macro, +constraint, rename and cross-attachment authority remain unqualified. + +All SDKs receive the shared records and the discovery RPC with codec vectors. +Only the reference advertises these new profiles. Ordinary providers reject +unsupported discovery; generated types do not qualify provider enforcement. +The native 1.5 adapter uses transaction-owned schema/object caches for nonempty +worker transaction handles on mutable catalogs. Frozen catalogs promise invariant +metadata and retain shared caches even when transactions scope function state. Committed `catalog_contents` snapshots cannot seed +those caches, and ending the transaction retires entries through the existing +bound-entry retention mechanism. Cross-schema CASCADE invalidates the complete +relevant cache. `make test_dependencies` exercises real native schema lifecycle, +concurrent metadata snapshots, rollback, reconnect and both cache settings. +Full SDK provider coverage, native 2.0 qualification, broader freeze and release +remain open. + +## Prepared read queries and typed parameters + +This development contract is part of the combined successor release. It does +not reserve a separate minor release. The Python worker/client and finite +reference provider implement this lifecycle. The native DuckDB 1.5 explicit +adapter is opt-in; automatic 2.0 integration and general provider qualification +remain open. Generated SDK records alone do not enable query execution. +`SET vgi_query_enabled=true` before ATTACH offers only explicit read queries; +the default remains false and existing attachments require reattachment. + +`ClientCapabilities.query_pushdown` is a nullable inline +`ClientQueryCapabilities` struct. It offers `encodings`, `semantic_profiles`, +the actual `engine_version`, and `max_query_bytes`. Absence selects no query +support. Unknown offers may be ignored. `CatalogAttachResult.query_pushdown` +is a nullable inline `QueryCapabilities` struct containing the selected +`encoding`, `semantic_profiles`, `execution_modes`, `backend_engine`, +`backend_version`, `capability_revision`, `default_evaluation_context`, and +`max_query_bytes`. The selected SQL limit is the minimum of the two limits. +The offer and selection travel inside the authenticated attachment; a later +capability or policy revision requires reattachment before more execution. + +The first encoding is `duckdb.sql.v1`. `vgi.remote.readonly.v1` selects +`explicit` provider-dialect execution. `vgi.duckdb.relational.v1` selects +`automatic` execution and requires transaction support plus provider +certification for the actual client engine build. A backend named DuckDB does +not establish semantic equivalence. No current reference advertises automatic +support. + +`QueryEvaluationContext` contains `timezone` (default `UTC`), `collation` +(`binary`), `default_order` (`asc`), `default_null_order` (`nulls_last`), and +`preserve_insertion_order` (`true`). Context equality is exact; the provider +must implement or decline the requested context. The context digest is SHA-256 +of `vgi.query.context.v1\0`, followed by the encoding, semantic profile, timezone, +collation, order, and null order, each UTF-8 encoded and prefixed by its unsigned +four-byte big-endian length, then one byte (`0`/`1`) for insertion order. +This framing is independent of Arrow IPC serialization details. + +### Parameter batches + +Parameters use the existing one-row `TypedValueBatch` carrier, with schema +metadata `vgi.query.parameters.v1` set to `named` or `positional`. There is no +parallel JSON type list. Every value retains its Arrow type, nullability, and +recursive field metadata. A typed NULL is a supplied value. Zero parameters +are an explicit one-row, zero-column batch with the mapping metadata. + +Named fields contain exact, unique, nonempty placeholder names without a +leading `$`, `:`, `@`, or `?`; numeric-only names are invalid. Positional fields +are exactly `1` through `N`, in order. One repeated SQL placeholder maps to one +field. Names and metadata are not normalized; the provider additionally checks +its dialect's identifier rules. Duplicate metadata keys, omitted values, +multiple rows/batches, trailing IPC data, and unsupported mappings reject. + +Preparation receives schema-only IPC. Each execution receives a full one-row +IPC batch with exactly that schema, including field order, names, types, +nullability, schema metadata, and nested metadata. Changes require another +preparation. Values remain bound parameters, never SQL interpolation. A +provider must fix binding to the declared types even when NULL-valued backend +parameters would otherwise produce value-dependent overload resolution. + +### Preparation and execution + +`catalog_query_prepare(request)` is a unary describe/authorize operation; it +must not execute the result query, including through a LIMIT 0 workaround. +The request fields are: + +| Fields | Contract | +| --- | --- | +| `attach_opaque_data`, optional `transaction_opaque_data` | Current authenticated attachment and, when supplied, its transaction. Empty handles reject. | +| `encoding`, `semantic_profile`, `mode`, `capability_revision` | Must match the immutable attachment agreement. | +| `query`, `parameter_schema`, `evaluation_context` | Bounded SQL, schema-only IPC, and inline context. | +| optional `catalog_version`, `catalog_etag` | Metadata snapshot hints; zero and empty etags are preserved rather than treated as absent. | +| `referenced_objects` | Inline `QueryObjectReference` records, each with an inline canonical `CatalogObjectRef` and schema-only `expected_schema`. Explicit mode requires an empty list. | +| optional `expected_output_schema`, `required_ordering` | Exact output contract when supplied; ordering is `unspecified` or `query_order`. | + +Automatic mode requires a transaction, metadata version, expected output +schema, and a nonempty table-only object manifest. Raw schema path components +remain attachment-relative. The provider must validate the complete logical +catalog, authorization, and data snapshot, not merely compare metadata versions. + +`CatalogQueryPrepareResult` is an exclusive accepted/declined union. A decline +has `status=declined`, a bounded `reason_code`, optional `reason_detail`, and +all acceptance fields NULL. A placement decline occurs before execution; +malformed input, authorization failures and backend errors remain errors. +Acceptance requires a nonempty opaque `prepared_query_token`, output schema, +result ordering, context digest and absolute expiry. Validated catalog +version/etag and row/byte estimates are optional, except automatic acceptance +requires the validated version. Required output schema and ordering must match. + +Execution uses TABLE `bind`/`init` with the reserved, schema-less name +`__vgi_catalog_query`, followed by the existing Arrow result stream. Its two +nonnull binary positional arguments are the preparation token and parameter +batch. The SDK owns one executor per Worker, hidden from catalog inventories. +It accepts only the full output projection and one reader (`max_workers=1`); +input batches, splits, secondary initialization, scan pushdown, time travel, +COPY, external settings/secrets, dynamic filters and cache validators reject. +The output schema, including metadata, remains exact throughout execution. + +Preparation tokens bind identity, attachment, transaction, capability revision, +SQL, parameter/output schemas and context. Bind resolves them without running +SQL. Each successful primary init starts a new execution; neither an ambiguous +failure nor a lost owner authorizes a retry or local fallback. Continuation +tokens resume an existing execution and must never reconstruct it by rerunning +SQL. The Python client uses `table_scan_continue` for reconnects and rejects the +older rebind-and-discard resumable path for this executor. + +`catalog_query_release(attach_opaque_data, prepared_query_token, +transaction_opaque_data=None)` idempotently releases an owner's preparation. +It remains usable for cleanup after expiry or capability withdrawal. Existing +executions have independent handles/deadlines and survive preparation release +or expiry. EOS, errors, cancellation, transaction end, attachment revocation +and execution expiry must release execution resources. Since cancellation is +best effort, providers need a janitor independent of client release calls. + +Protocol bounds are SQL 1 MiB, request/result envelopes 8 MiB, parameter batch +8 MiB, schema-only IPC 1 MiB, opaque tokens 64 KiB, 1,024 parameters/referenced +objects, and 32 schema-path components. Recursive parameter metadata validation +is bounded at depth 64. The SDK limits preparation lifetime to one hour and +execution lifetime to 24 hours; providers should choose shorter leases and +enforce resource-count and storage quotas. + +The SQLite reference stores immutable preparations and ordered Arrow fragments +in shared storage. It binds only six exact read queries over scalar parameters +and an immutable seven-row relation, with no general SQL sandbox or automatic +equivalence claim. It materializes results during init, uses two-minute leases, +64 resources and a 16 MiB result-storage quota, and exposes an independent sweep +operation for abandoned resources. Public HTTP tests cover typed NULL rebinding, +reconnect to a different worker, cancellation, release, transaction end, +ownership, schema mismatch, lost results, expiry and policy changes. General +provider authorization/snapshot integration, a scheduled production janitor, +native explicit/automatic adapters and performance qualification remain open. + + +## Explicit transactional statements — October 25, 2026 + +This bounded extension reuses `catalog_query_prepare`, `catalog_query_release`, +`__vgi_catalog_query` bind/init/stream, typed parameter batches and the existing +transaction RPCs. It adds no method or request/response field. It adds negotiated +semantics and reserved result-schema metadata to the unpublished successor. +`vgi.remote.readonly.v1` and automatic read-query semantics remain unchanged. + +`vgi.remote.statement.v1` selects execution mode `statement`. Selection requires +an explicit client offer and provider transaction support. Every request must +carry a nonempty existing transaction handle and the exact provider-default +evaluation context. It has no automatic object manifest. The provider admits +one explicitly requested DML or transactional DDL statement in its qualified +SQL subset. Scripts, transaction control, ATTACH/DETACH, session mutations, +COPY/external effects and nontransactional DDL are outside this profile. +Unsupported syntax/semantics decline before effects; authorization, malformed +requests and backend failures remain errors. Merely naming a backend DuckDB +does not establish support. The initial provider is a finite test reference. + +Preparation and bind do not execute mutations or infer types by running them. +Accepted `output_schema` carries exactly one semantic result-kind value under +`vgi.statement.result.v1`: + +| Value | Fixed Arrow result contract | +| --- | --- | +| `command` | Exactly one batch containing one row and one nullable int64 field named `affected_rows`. DML supplies a nonnegative affected-row count (including zero); DDL supplies NULL. Field metadata is empty. | +| `rows` | Nonempty declared output schema for RETURNING; zero or more result batches/rows. Empty results still require successful EOS. | + +The metadata and schema are fixed at preparation and retained on every batch. +The reserved key is rejected on read-profile preparations. A count row alone +is not completion: successful EOS completes the statement, not the transaction. +There is no separate completion RPC, trailer or durable receipt/replay service. +Result validation failures and early cancellation invalidate the entire existing +transaction. Providers reject commit while a statement is incomplete or failed; +transaction end, owner loss and expiry reclaim executions and roll back as +needed. No retry or local fallback follows an ambiguous execution failure. +An explicit caller must consume to EOS; constructing a lazy Python generator +alone executes nothing. Reusing a preparation for an intentional new execution +is distinct from retrying an ambiguous old one. + +The command response does not need a post-execution catalog-version field. +After DDL, clients use existing `catalog_version` and catalog discovery under +the same transaction, then refresh after commit. `validated_catalog_version` +in preparation remains a pre-execution observation and must never be rewritten +as the post-DDL version. Native statement adapters must invalidate local caches +conservatively before another lookup in the same transaction. + +Python implements the profile validation and command-result shape checks. The +six other worker SDKs retain compatible data-only query records and default +UNIMPLEMENTED query handlers; they do not gain executable statement support. +The native 1.5 `vgi_query` offer/validator still admits only read-only queries. +A dedicated mutation-aware native statement entry point is unimplemented. +Runtime evidence and limits belong to the companion manifest checkpoint. + +## Combined protocol review — October 23, 2026 + +This source review reconciles the complete field inventory with the implemented +development contract. It closes scope/mapping review, not P1 semantic proof, +P2 freeze or P3 release. No runtime shapes, version labels or engine pins change +in this checkpoint. Original audit evidence and historical runtime results remain +intact in the [manifest](duckdb-2.0-manifest.json); its +`combined_protocol_review_2026_10_23` records the remaining work and deferral costs. + +### Remaining included column and cleanup changes + +| Existing field | Successor contract selected by this review | +| --- | --- | +| `catalog_table_column_default_set.expression` | Required `binary` carrying exactly one canonical `SqlExpression` IPC record, replacing raw SQL text. SQL NULL is an expression value; DROP DEFAULT stays its own existing method. | +| `catalog_table_column_type_change.expression` | Nullable `binary` carrying the same record. NULL requests the qualified ordinary cast path; a present record defines USING against the old row. | +| `CatalogAttachResult.default_schema` | Nullable scalar string. NULL means no implicit default; a present value is one nonempty, non-NUL raw root identifier. Existing providers can still explicitly return `main`. No empty-string coercion, dot splitting, nested search path or invented default. | +| Macro create/read definition | Required nonempty overload family; a single macro is a one-element family. Parent definition, parameters, defaults and arguments-schema fields are removed. Local convenience helpers normalize simple definitions into the canonical family. | + +The DEFAULT/USING payload migration is implemented in the development branch, +including generated SDKs and canonical CREATE/ADD default metadata. Per the +October 23 implementation decision, these are **mandatory successor wire +representations**, with no new default/USING capability negotiation. The semantic +`profile` remains part of `SqlExpression`; it identifies the interpretation of +the SQL, not a new permission flag. Unknown profiles, malformed records and +owner mismatches reject before provider effects. + +Nullable default schema is now implemented in the development protocol and SDKs. +Python's attach result and declarative `Catalog` accept `default_schema=None`. +Omitting the constructor argument retains the existing `"main"` default; +explicit NULL stays absent through attach responses, catalog discovery, clients +and codecs. A present value must be nonempty and contain no NUL; dots are literal +characters in one root identifier. A catalog without a default must register its +functions in explicit schemas instead of relying on an implicit registration +schema. No capability flag or extra field is needed. + +The native 1.5 adapter rejects absent defaults before publishing an attachment +or registering its settings. It also rejects empty/NUL names instead of +coercing them to `main`. Present defaults, including literal dotted identifiers, +retain their behavior. Native 2.0 can later map absence to its optional catalog +API; that execution path is not qualified by the 1.5 tests. + +Macro create/read now share the canonical family representation in this same +major transition. The Python client, provider hook and worker use the existing +catalog methods for the complete family. No overload-specific opt-in is needed +for this mandatory wire shape. Types, documentation and ordered parameters +remain per overload; discovery returns resolved literal defaults, including +typed NULLs, and rejects unresolved expression defaults. Function argument +defaults remain a separate, unchanged contract. See the +[macro contract](macro-contract.md) for the native adapter's bounded support +and remaining qualification work. + +Successor column CREATE/ADD use canonical base64 IPC field annotations for +defaults. Native 1.5 also translates supported virtual generated definitions +on CREATE and discovery; its parser rejects ALTER ADD of generated columns. +Comment remains `comment`; +tags remain `tags`, encoded as a UTF-8 JSON object with literal string keys and +values. Reject duplicate decoded keys (including escaped-equivalent keys), +malformed UTF-8/JSON, unpaired Unicode surrogates and non-string values before +provider effects or table publication. Preserve explicit empty comments/maps, +literal dots, Unicode and NUL, and unrelated metadata. Validation traverses +container, dictionary and extension children. No generic metadata RPC or new +expression envelope is selected. Schema/table/index option values remain typed +literals. + +The existing keys have mandatory meanings in this successor contract; no +`vgi.column.metadata.create.v1` opt-in is required. Python's `ColumnMetadata`, +`column_metadata`, `with_column_metadata` and `validate_column_metadata` helpers +provide authoring and validation without changing the wire schema. CREATE, ADD +and discovery validate the same values. The durable column reference retains +them in its existing transaction snapshots and readback. + +Native 1.5 forwards column comments on CREATE/ADD and imports them on discovery, +preserving absent versus empty text. It validates and retains tag annotations in +the authoritative Arrow schema. Its `ColumnDefinition` has no public tags API, +so SQL authoring/introspection of column tags and tag preservation through +reconstruction using only native columns remain native-adapter limitations. +Nested comments likewise remain Arrow metadata where the engine lacks a nested +column-comment API. No engine-private access or engine-pin change is introduced. + +The proposed `vgi.column.default.v1`, `vgi.column.metadata.create.v1`, +`vgi.column.type.using.v1` and `vgi.column.generated.virtual.v1` capability flags +are withdrawn: the canonical representations need no additional opt-in. +`vgi.column.default.v1` remains an Arrow metadata key, independently of +capability names. A codec accepting an expression record does not establish +provider execution support. + +DEFAULT/USING now use the mandatory canonical representation even with no +catalog-capability offer. Existing nested-operation, transaction and constraint +requirements still apply where they already applied. DROP DEFAULT remains its +own operation. Providers validate their supported grammar/context and return +ordinary unsupported errors for operations they cannot implement. A TYPE-only +column definition cannot carry default/generated metadata at any nesting level. +The virtual generated-column checkpoint below qualifies a bounded CREATE/read +path. Unsupported generated mutations and grammar remain ordinary errors. + +The durable column reference preserves canonical SQL/context through CREATE, +ADD, SET/DROP DEFAULT, USING, discovery and transaction snapshots. The native +1.5 adapter supports a bounded deterministic scalar grammar under the explicit +default evaluation context, including numeric division. It materializes supported +defaults as typed literals for later native binding while retaining the original +record in authoritative table metadata. Unsupported contexts and volatile/general +SQL reject; the generated path has its own bounded qualification below. + +Existing static `Table.defaults` descriptor discovery retains its legacy read +adapter; it is not silently interpreted as new canonical metadata. The custom +DuckDB transactor fixture cannot preserve canonical expression contexts and now +rejects those DEFAULT/USING operations explicitly. Its plain casts and expression- +free CREATE/ADD remain available. The durable reference supplies the tested +canonical mutation behavior; no transactor or native2 qualification is implied. + +Virtual generated definitions pair `vgi.column.generated.v1` (canonical base64 +`SqlExpression` IPC) with `vgi.column.generation.v1 = virtual`. The Arrow field +supplies the resolved result type. Authoring conveniences translate their SQL +strings locally using the owning schema and explicit default evaluation context. +Discovery and CREATE reject legacy-only `generated_expression`, mixed legacy +and canonical forms, incomplete pairs, malformed records, wrong owners and +unsupported interpretation profiles. The generic category codec retains +`stored`, but current catalog admission rejects it before provider effects or +publication; it is not an executable feature. + +The native 1.5 checkpoint supports top-level deterministic virtual expressions +over signed integer, Boolean and string columns under the default context. +Dependencies, cycles and declared result types are checked before publication. +The worker returns physical fields in their original relative order; DuckDB +computes virtual results. Generated fields must not consume physical indices. +The durable generated reference reuses the existing dependency authority and +transaction snapshots, including physical scan readback after reconnect. + +This checkpoint does not qualify native INSERT/UPDATE/DELETE on generated +tables, stored or nested generation, or semantic generated-column rewrites. +Those paths reject before effects. Native ALTER ADD of generated columns remains +rejected by the pinned parser. The durable reference rejects rename/drop/type +column edits unchanged; the earlier metadata-only dependency reference retains +its separately bounded rewrite behavior. The native proof covers a single +worker-function scan; delegated, multiple-branch and partitioned generated scans +reject pending separate qualification. No new RPC, record schema, engine pin, +capability flag or public protocol version is introduced. + +Views keep SELECT text, owner-relative late binding and derived output schemas; +no fixed persisted output schema or implicit caller search-path contract is +introduced. Existing aliases, security, comments/tags and column comments remain. +Dependency `owned` edges describe drop lifetime, not principal authorization: +SQL ownership transfer is deferred. Non-default physical compression is also +deferred until a provider defines portable semantics. Existing typed options or +metadata may suffice for a future compression profile. + +Keep `ClientCapabilities.filter_encodings`: the historical proposal to remove +it has no demonstrated consumer conflict. Typed function defaults remain +authoritative; display-only `vgi_default` metadata is not a second executable +default contract. Raw paths and canonical constraints already provide the other +selected cleanup; no parallel replacement RPC families are needed. + +### Optimizer work that still blocks P1 + +Projection and repeatability remain included to avoid an immediate second wire +migration, but their final fields are **not frozen**. Existing split tokens, +parallelism limits, ordering and string diagnostics remain sufficient for the +reviewed engine hooks; no typed metric or selected-partition-ID record is selected. + +Projection needs a closed scalar profile, exact result types and deterministic +original-input/result-slot mapping. Filter v2 currently requires a Boolean root +and types its calls as Boolean; its grammar cannot simply be relabeled a scalar +projection contract. A bounded first proof can use ordinary CAST of a source +column, keeping TRY_CAST and other functions unsupported until specified. + +The support decision must precede optimizer replacement. Current native +`table_function_plan` runs during global initialization, so a decline in its +response arrives too late. Prefer a negotiated closed subset the native callback +can decide locally, then carry the accepted plan through existing plan/init +requests and their exact output schema. If support depends on provider state, +prove a pure preparation round trip before replacement. This lifecycle choice +and the exact record fields remain P1 work. Declined expressions retain local +evaluation and all needed input columns; malformed accepted results are errors, +never execute-then-fallback. + +Prove nested paths, reordered outputs, residual/runtime filters, bind copies, +split fingerprints and cache identities use the same mapping. No general SQL +bag, serialized private AST, or speculative result-identity framework is selected. + +Repeatability needs a bind-specific claim, initially absent/unknown versus +stable within one query, stored with bind data. Prefer one enum over an empty +wrapper record. The same bind, parameters and transaction/snapshot must yield +the same logical result when reevaluated. Cache TTL, scalar stability, replayable +split tokens and worker count do not prove that guarantee or authorize retries. +Before freezing, demonstrate mutable-provider behavior, prepared bind reuse, +snapshot expiry and repeated evaluation without inventing a new query coordinator. + +### Reviewed deferrals and rollout + +New trigger CRUD/execution, sequence allocation, named-type catalog APIs, +collation/CRS catalogs, standalone windows, arbitrary mutating statements/CONNECT, +multiple relational input transport and typed metrics are deferred. Existing +aggregate windows, read-query execution, supported Arrow value transport and +transaction/write phases remain. The manifest records each actual missing +semantic guarantee, inclusion proof, user-visible limitation and future version +cost. Deferred candidate names are not part of the included freeze inventory. + +No new query parameter carrier is needed: types/values use Arrow batches with +the existing mapping metadata. No additional TUPLE/VARIANT carrier is selected: +keep recursive tuple annotations and canonical unshredded Arrow Parquet Variant, +with explicit engine mapping limits. Adapter qualification does not follow from +opaque metadata/byte preservation alone. + +The successor requires a major version and matching routing name; the historical +additive `2.2.0` successor candidate is superseded. Published main has independently reached +`2.2.0`; that release is the integration baseline, not the final successor version. Choose the +number at freeze. Keep exact +major/minor checks and deploy separate versioned endpoints/processes. Drain old +transactions, then reattach and reprepare; never transfer tokens or retry an +ambiguous mutation during migration. Rollback needs old-compatible catalog +objects or a tested restore/migration. P3 must exercise these steps and safe +unsupported behavior. No version-aware router or recovery service is required. + +Validation on replacement EC2 `98.81.121.120` passed: `--require-decisions` covers +151 audit rows and 20 areas with zero unresolved scope mappings; all 11 manifest +tests pass. Logs: `logs/protocol-review-20261023-SBCl67`. P0 is complete; these +inventory checks do not close P1 or qualify new runtime behavior. + +## Combined expression, constraint, transaction and trigger design + +**Checkpoint: 2026-10-14. Status: SQL/context and canonical constraint records +generated; bounded public constraint migration verified on pinned 1.5; remaining APIs +specified and not frozen.** The definitions in this +section supersede placeholder records in the manifest and the older trigger +proposal. They belong to the same major transition as the existing development +changes. Engine pins remain unchanged; the public inventory now has 84 methods. +The 2026-10-15 scope correction below supersedes the earlier transaction and +durable recovery proposals; it changes documentation only. +A specified shape does not close P1. The implementation evidence below applies +only to its stated scope; the remaining acceptance cases still require proof. + +### SQL and constraint implementation checkpoint + +`SqlExpression`, `SqlStatement`, `MacroOverload`'s required context and all six +canonical constraint records are implemented and generated across seven SDKs. +`CatalogObjectRef(kind="constraint")` requires an owning table and preserves +the provider-issued key exactly. Codecs reject malformed or conflicting records; +they do not authorize execution or prove enforcement by a provider. + +The durable SQLite-backed constraint reference stores Arrow rows and canonical +records together. It proves create/read/atomic apply, immediate NOT_NULL, +UNIQUE, PRIMARY_KEY, BOOLEAN CHECK and MATCH SIMPLE foreign keys, existing-row +validation, exact referenced-key dependencies, schema nullability, stable keys, +effect-sensitive revisions, rename/recreate, transaction isolation, rollback, +reconnect and failed-publication recovery. DuckDB binds and evaluates SQL rather +than using text matching to simulate checks. Its finite scope is top-level +boolean, signed integer and string columns; only ENFORCED + ALL_ROWS and +NO_ACTION/RESTRICT foreign-key actions execute. Retargeting an existing foreign +key, nested constraints, alternate enforcement modes and broader actions reject. + +The shared scalar reference accepts constants, primitive casts, row columns, +arithmetic `+ - * %`, comparisons, AND/OR/NOT, NULL tests and +`abs/lower/upper/coalesce`, under the explicit default evaluation context. +CHECK requires BOOLEAN; TRUE and typed NULL pass. DEFAULT rejects row references. +Generated-column evaluation orders a finite dependency graph and rejects cycles. +Only the existing USING adapter additionally permits its already qualified nested +old-row references and numeric literals. Other contexts, functions and logical +types reject. DEFAULT/generated/USING helpers prove evaluation rules; they do not +establish a new persistent default/generated-column implementation. `SqlStatement` +is a validated carrier only, with no new statement execution permission. + +The shared corpus covers all constraint kinds and mutation variants, binary +revisions, nullable fields, raw identities, required contexts and exact expression +IPC preserved inside column metadata across all seven SDKs (both TypeScript +Arrow backends). Arrow metadata is UTF-8: raw IPC there failed Rust decoding, so +the metadata encoding below uses canonical base64. Record fields typed `binary` +continue carrying raw IPC. The native 1.5 adapter rejects these new column +metadata namespaces recursively until their semantics are implemented. + +### Public constraint migration checkpoint + +The parent table records now use the canonical fields below, and +`catalog_table_constraints_apply` is a public RPC. Python gates creation, +discovery and mutation on selected profiles and current provider support. +Mutation takes the discovered semantic revision; it does not retry an ambiguous +result. Unported SDK providers reject constrained creation and unsupported +mutation before changing state. Read-only providers must explicitly supply +canonical identities and trust states; legacy arrays cannot manufacture those +facts. Generated schemas and registries cover all seven SDKs. + +The durable reference exposes public creation, discovery, apply and DDL +transactions. It advertises no public scan/write functions. Its provider-side +row replacement authority continues validating the same persisted constraints. +The native 1.5 subset translates top-level NOT_NULL, UNIQUE, PRIMARY_KEY and +same-schema MATCH SIMPLE NO_ACTION/RESTRICT foreign keys. SET/DROP NOT NULL uses +canonical apply with the discovered revision. At that checkpoint CHECK, nested +paths, unsupported trust states and broader FK actions rejected explicitly. +The October 23 checkpoint below adds bounded CHECK discovery. The October 24 +nested-NOT_NULL follow-up additionally imports enforced, ALL_ROWS facts for +ordinary STRUCT field paths. It validates exact raw components and recursive +Arrow nullability, preserving the original canonical records. A nonnullable +child does not make its parent nonnullable: only top-level NOT_NULL facts become +DuckDB 1.5 constraints or supply top-level ALTER identities. Writes still require +the negotiated provider enforcement profile. Collection and logical-type storage +children do not become SQL constraint paths. Nested UNIQUE/PRIMARY_KEY/FK, nested +SQL constraint DDL, CHECK creation and other unsupported semantics still reject. + +EC2 validation passed for the Python public lifecycle, all SDK contract/adapter +suites, the bounded native 1.5 adapter and six built-worker discovery lanes. +Java's shared RPC serializer now preserves declared Arrow dictionary encodings +for these embedded records; strict IPC and cross-language tests cover the fix. +SDK fixture declarations are METADATA_ONLY/UNVALIDATED, without an enforcement +or native trusted-materialization claim. Exact counts, skips and logs are in +the manifest's `constraint_public_checkpoint`. + +The October 24 full-language SQL follow-up supersedes those unqualified example +fixture declarations. The read-only examples validate facts against the same +immutable rows emitted by scans and publish matching Arrow nullability and +version-owned constraint snapshots. Their attach selection advertises and selects +only supported, offered read profiles; it adds no generic enforcement or write +claim. Ordinary unconstrained fixture columns remain nullable. The complete +configured seven-language matrix and explicit skips are recorded under +`full_language_slt_checkpoint_2026_10_24`. + +This checkpoint adds one method and changes parent schemas; no version is +published or release gate closed. Generic legacy +write/bind authorization remains provider-owned: preliminary table reads are +not atomic with later operations. Later checkpoints implement typed indexes and +semantic ALTER dependencies. Trigger execution remains pending; the 2026-10-15 +scope correction replaces the earlier statement lifecycle/replay proposals. + +### Shared SQL definitions and evaluation phases + +Keep `SqlExpression(sql, profile, schema_path)` for exactly one **scalar** SQL +expression and add required `evaluation_context: QueryEvaluationContext`. +Reuse the existing context record and its explicit timezone, collation, order, +NULL order and insertion-order settings. Do not create another settings map or +infer semantics from a backend's name/version. Profiles must specify every +additional relevant engine setting as fixed or unsupported; the context record +is not permission to inherit arbitrary session state. + +Add `SqlStatement` with those same four fields for one query/DML statement. +Its selected semantic profile specifies the allowed statement kind. It cannot +contain scripts, transaction control or unrestricted statement execution. +Scalar expressions, table-macro queries and trigger actions have distinct +profiles and bind scopes despite sharing text/context conventions. No private +AST, bound-plan serialization or unversioned engine-to-SQL map crosses the wire. +The context digest used by existing queries is unchanged; reusing the record +for persisted definitions does not make their digests query tokens. + +The receiving authority binds the definition in the containing operation's +transaction, under the raw owner `schema_path` and declared row/parameter scope. +The containing request supplies the table/object and target `ColumnPath`. +No client catalog alias, implicit caller search path, cross-attachment target or +temporary dependency is permitted for a persistent definition. Binding resolves +types, casts, permissions and dependencies before effects. A client's claimed +dependency list is neither authoritative nor authorization. Readback preserves +the SQL/profile/context and resolved type without claiming source whitespace +round trips. Renames either rewrite/rebind all affected definitions atomically +without changing meaning, or reject; cache invalidation alone is insufficient. + +| Use | Binding and evaluation rule | +| --- | --- | +| Column DEFAULT | Bind at definition time without row references or query parameters. Evaluate for each row requiring a default at INSERT/defaulted UPDATE, not universally at CREATE. Deterministic scalar support does not imply volatile, sequence, subquery, aggregate or window support. | +| ADD COLUMN | Distinguish population of existing rows from the future-write default. Preserve NULL parent structs for nested fields. An engine-expanded ADD/UPDATE/SET DEFAULT is one atomic statement, even if implemented by multiple calls. | +| Generated column | Store generated category, expression and declared/resolved Arrow type. The first profile targets deterministic virtual generation. Bind in dependency order and reject cycles. Stored generation needs its own qualified rewrite profile. | +| CHECK | Bind to the candidate row. FALSE rejects; TRUE or NULL passes. Enforced addition validates existing rows and installs future checks atomically. | +| ALTER TYPE USING | Evaluate once per affected row against the old schema, then apply the specified target-type cast. Validate data, dependent definitions and schema together; never substitute a future DEFAULT for USING. | +| Partition, sort and index key | Deterministic row expressions. Layout retains explicit direction/NULL order and REWRITE/FUTURE_WRITES semantics; an index or physical layout does not imply scan ordering. | +| Macro default | Retain `duckdb.macro.default.v1`: the owner evaluates once at CREATE and discovery returns the resolved typed literal. It is not an invocation-time closure over other parameters. | +| Macro body | Scalar bodies use scalar scope; table bodies use query scope. `MacroOverload` gains required `evaluation_context`; supplied default-expression contexts must match it. Keep overload/parameter identity. | +| Trigger action | One profiled DML statement with explicit transition-relation scope and execution owner. It is not a scalar expression or a general remote SQL execution capability. | + +In the combined breaking transition, versioned Arrow field metadata keys +`vgi.column.default.v1` and `vgi.column.generated.v1` carry canonical RFC 4648 +standard padded base64 ASCII of single-record IPC `SqlExpression` values; +`vgi.column.generation.v1` carries `virtual` or `stored`. Reject raw binary, +whitespace, noncanonical padding, invalid alphabet and oversized metadata before +decoding; never interpret an alternative encoding by guesswork. +The containing Arrow field remains authoritative for the logical type. Absent +default metadata means no declared default; a SQL expression `NULL` is a +declared default. Removal uses the existing explicit mutation, not an empty +payload. Generated category and expression must occur together. Reject legacy +`default`/generated metadata alongside these authoritative keys rather than +choosing one silently. Exact decoded IPC preservation is covered by the shared +SDK corpus. Native 1.5 now translates supported CREATE/ADD defaults and reads +canonical default metadata; existing static descriptor discovery retains its +legacy adapter. Default/USING mutation payloads now carry `SqlExpression` IPC; +no second column-RPC family is added. CHECK and index definitions use the same record. + +The `vgi.expression.duckdb.scalar.v1` reference proves only the finite subset and +roles listed in the implementation checkpoint. It does not qualify broader SQL, +volatile defaults, stored generation, general statements or native translation. + +### Canonical constraints and index definitions + +Replace the five parallel constraint collections in `TableCreateRequest` and +`TableInfo` in this major transition. There is one authoritative representation: +creation carries `constraint_definitions: list` of `ConstraintDefinition`; +discovery carries `constraints: list` of `ConstraintInfo`. Both lists +are required; empty means no constraints. CREATE definitions request +ENFORCED + ALL_ROWS and require the enforcement profile; reject any downgrade. +Metadata-only creation is not implied by this request shape. Native adapters translate engine +logical indices into raw paths at the boundary. They must not transmit physical +indices, synthetic row IDs, backing-index OIDs or both legacy and canonical forms. +An absent read profile cannot be handled by hiding enforced constraints or +returning a false empty list: reject materialization when the client cannot +honor the table's required semantics. + +| Record/field | Proposed wire value and invariant | +| --- | --- | +| `ConstraintDefinition.declared_name` | Nullable UTF-8 SQL label. Non-NULL labels are unique within the table under the provider's identifier rules. It is not mutation identity. | +| `kind` | Enum `NOT_NULL`, `UNIQUE`, `PRIMARY_KEY`, `CHECK`, `FOREIGN_KEY`. | +| `column_paths` | Ordered, non-NULL raw `ColumnPath` list. NOT_NULL has one; UNIQUE/PRIMARY/FK have nonempty duplicate-free paths; CHECK has none. | +| `check_expression` | Nullable `SqlExpression` IPC; required exactly for CHECK. | +| `foreign_key` | Nullable `ForeignKeyDefinition` IPC; required exactly for FK. | +| `ForeignKeyDefinition.referenced_table` | `CatalogObjectRef` for a table in the same attachment; no client alias. | +| `referenced_column_paths` | Ordered, duplicate-free paths with the same arity as the local key. | +| `match`, `on_update`, `on_delete` | Explicit enums: SIMPLE/FULL and NO_ACTION/RESTRICT/CASCADE/SET_NULL/SET_DEFAULT. No MATCH PARTIAL or deferred checking is implied. | +| `ConstraintInfo.ref` | Sole identity: `CatalogObjectRef(kind="constraint", schema_path, owner_table, name)`. `name` is a provider-issued persisted logical key, compared exactly and never reused; it is not an engine OID or SQL label. | +| `definition` | Complete canonical `ConstraintDefinition` IPC. | +| `enforcement`, `validation` | ENFORCED/METADATA_ONLY and ALL_ROWS/FUTURE_WRITES/UNVALIDATED. Only ENFORCED + ALL_ROWS may become trusted engine constraint facts. | +| `referenced_constraint` | Nullable canonical constraint ref; required for an FK and NULL otherwise. Provider binds the actual referenced UNIQUE/PRIMARY key at creation. | + +Extend `CatalogObjectRef` to accept `constraint` with required `owner_table`, +as it already does for triggers. Changing a declared label preserves the +logical key. Table/schema renames update the owning coordinates and all graph +references atomically. An engine that discarded a SQL label reports NULL; +it cannot invent a preserved user declaration. Providers unable to persist +logical keys cannot select canonical mutation support. + +Add one mutation method, `catalog_table_constraints_apply(request)`, replacing +the manifest's provisional separate add/drop methods. Its request contains +attachment, optional transaction, raw table identity, required nonempty +`expected_revision: binary`, and ordered nonempty changes. Read discovery's +`TableInfo.definition_revision` before mutation; a stale revision rejects. +Add this nullable binary field to discovery; it is required nonempty whenever +canonical mutation support is selected. It changes on relevant schema, +constraint, enforcement or dependency changes, including changes through existing +column RPCs, and is never reused across DROP/recreation. No-op mutations retain +it; comment-only changes may change the catalog version without changing this +semantic revision. A change is a strict union: + +| Change | Fields and behavior | +| --- | --- | +| ADD | Complete definition; requested enforcement/validation. Allocate identity only when the whole request succeeds. | +| DROP | Canonical constraint ref and explicit CASCADE boolean. Missing identity errors; no silent retargeting by SQL label. | +| RENAME | Ref and nullable replacement declared label; key and semantics remain unchanged. | +| REPLACE | Ref, complete replacement definition and requested enforcement/validation. Retains the key, but revalidates every dependent binding. | + +The response is `ConstraintSnapshot(table, definition_revision, constraints)` +from the successful mutation snapshot. All changes validate and publish together; +unsupported kinds/actions reject before mutation. Multiple changes may not +address the same existing key in one request; a newly allocated ADD cannot be +addressed until readback. Existing SET/DROP NOT NULL operations update this same +canonical state and obey the same dependencies. Their native grammar remains +available; they do not maintain a second constraint collection. + +ENFORCED + ALL_ROWS requires validation of existing data and enforcement for +all subsequent writes, including COPY, other clients and provider-side writes. +A metadata-only declaration is never an enforcement promise. PRIMARY permits +one primary key and contributes NOT NULL. Dropping it removes a field's NOT NULL +property only when no independent enforced requirement remains. UNIQUE permits +NULL-containing keys under the selected SQL profile. Arrow `nullable=false` +and canonical enforced NOT NULL/PRIMARY semantics must agree in both creation +and discovery. Metadata-only/unvalidated declarations cannot justify narrowing +Arrow nullability. Nested STRUCT paths observe visible values: a NULL parent +hides its children. List indexing/traversal is not implied. + +Baseline FK execution is immediate MATCH SIMPLE with NO_ACTION/RESTRICT. Any +NULL local component exempts a SIMPLE row; FULL permits all NULL or no NULL. +FULL and each propagating action require their additional selected profile. +SET_NULL/SET_DEFAULT must still satisfy affected constraints. CASCADE must +atomically apply all effects or fail. Cross-attachment targets reject. Broader +same-attachment cross-schema references can be implemented by providers; the +native pin's narrower same-schema rule still applies. Changing/dropping a +referenced key cannot silently retarget an FK to another available key. + +Retain existing index create/get/list/drop RPCs. Replace string expressions and +stringified options in their successor records with an `IndexDefinition`: +`table: CatalogObjectRef`, `algorithm: string`, ordered nonempty `keys`, +`options: TypedValueBatch`, explicit `constraint_type: NONE|UNIQUE|PRIMARY`, and +nullable `associated_constraint: CatalogObjectRef`. Each key contains exactly +one raw column path or `SqlExpression`; implicit ordering/collation is forbidden. +Expression context supplies collation. For a raw column key, the initial profile +admits only explicitly established binary column collation; it rejects a +nonbinary or unresolved column collation. Broader collation support remains +part of the separate candidate review and cannot be inferred from an algorithm +name. Physical key-order options require an algorithm profile. A UNIQUE index does not silently create a logical PRIMARY +constraint. Constraint-associated indexes require an authoritative association; +PRIMARY without one rejects. Add `catalog_index_option_definitions` using the +existing `OptionDefinitions` representation. `IndexInfo` returns effective +options through `OptionState` and the complete resolved definition. Unknown +algorithms, options, types or expressions reject before index construction. + +The implemented carrier uses `IndexKey.column_path: list?` and +`IndexKey.expression: binary?`, exactly one present. `IndexDefinition.table`, +`options`, and optional `associated_constraint` are nested IPC records; +`keys: list` preserves key order. Enum wire values are `NONE`, `UNIQUE`, +and `PRIMARY`. `IndexInfo.definition` and `IndexInfo.options` are required binary +records; the effective values in `OptionState` must equal the definition options, +including Arrow metadata. `IndexCreateRequest` keeps index identity, conflict +mode and attachment/transaction tokens and replaces the legacy expression and +string-option fields with the required definition. Index identity and target +table identity may have different schema paths in the general protocol. + +`catalog_index_option_definitions(attach_opaque_data, table, algorithm, +transaction_opaque_data?)` accepts a table-reference IPC record and returns the +existing `OptionDefinitions` record. Algorithm identity is explicit: the bounded +SQLite reference uses `SQLITE.BTREE.V1`, never `ART`. This spelling survives the +pinned DuckDB parser's uppercasing of USING identifiers. The reference creates +actual regular/UNIQUE SQLite indexes and declares an empty option schema. The +native 1.5 adapter supports raw top-level keys in one flat schema with binary +collation; expressions, nested keys, nonempty options, PRIMARY/constraint +associations and CASCADE reject before effects. Richer typed values and keys +have codec coverage without implying provider support. + +Profiles use existing `CatalogCapabilities` categories: read +`vgi.constraint.records.read.v1` and `vgi.index.definition.read.v1`; DDL +`vgi.constraint.create.v1`, `vgi.constraint.mutate.v1`, +`vgi.index.definition.create.v1`, `vgi.index.drop.v1`; write +`vgi.constraint.enforce.v1`. DDL profiles require corresponding readback. +Define `vgi.constraint.fk.match_full.v1` and action-specific +`vgi.constraint.fk.{cascade,set_null,set_default}.v1` as additional write +profiles. All accepted requests must satisfy the profile; a provider may reject +unsupported constraint kinds, mutation forms or algorithms before effects. +Constraint read/create/mutate/enforce and index read/create/drop profiles are +implemented. Index create/drop require index readback; REPLACE additionally +requires drop, UNIQUE requires constraint enforcement, and associated-index +mutations require constraint mutation authority. Transactional DDL requires its +existing transaction profile. Additional FK match/action profiles remain pending. + +The inspected 2.0 pin supports CREATE-time constraint forms but accepts only +UNIQUE/PRIMARY through general ADD CONSTRAINT; SET/DROP NOT NULL have separate +paths. DROP CONSTRAINT is rejected by its transformer. General provider mutation +support is still included in this protocol design. Native adapters reject forms +the engine cannot express rather than simulate unrelated SQL. Existing native +index codecs alone are not evidence of physical construction; the bounded +reference and native SQL lifecycle checks provide that separate evidence. + +### Dependencies for semantic ALTER + +`ObjectDependency.kind = requires|owned` controls DROP behavior. Required +`blocks_alter: bool` and nullable, nonempty `subject_column_paths` specify +semantic ALTER protection; NULL protects the whole subject. Harmless comments +or label changes do not count as semantic ALTER. Edge identity is the dependent +ref, nullable dependent column path, and subject ref. Merge subject paths only +within that identity; reject conflicting edge kinds or ALTER flags. A NULL path +set dominates narrower protection. Explicit parent and child paths remain in the +merged set so authoritative schema validation checks every claimed field. +Protecting a parent path also protects its nested descendants, and changing a +parent invalidates dependent child paths. DROP follows dependencies regardless +of `blocks_alter`; clearing the flag cannot permit dangling column references. + +Constraint, trigger and index refs participate in the same graph. A constraint +is owned by its table; an FK requires the exact referenced key and referenced +columns. A dedicated backing index is owned by its constraint and the constraint +requires the index. That mixed `requires`/`owned` cycle is legal; ownership cycles +remain invalid. A shared index is independently owned and required by each user. +DROP INDEX RESTRICT cannot disable enforcement. A supported CASCADE or replacement +must authorize and atomically resolve the complete closure, including FKs. + +Generated-column dependency ordering uses nullable +`dependent_column_path`. This identifies the column within a dependent table; +it is NULL for object-level dependents. Same-table column edges become legal +only with distinct, valid column targets. Build cycle nodes from full object +and column identity; multiple generated columns in one table do not collapse +into a single self edge. Their column dependency graph must be acyclic. +The Python planner validates column existence against authoritative schemas and +returns separate object and column deletion sets. A generated-column edge cannot +turn a column deletion into deletion of its owning table. Providers must authorize +the complete closure and recheck it in the same transaction that publishes the +change; a client-side graph read is advisory. Whole-object requires cycles and +the backing-index mixed cycle above remain legal, while evaluation cycles +involving a dependent column are invalid. Table-column references traverse +column computations rather than unrelated whole-table lifetime dependencies; +view-column references also traverse the whole view definition. Ownership cycle +checks retain the complete lifetime graph. + +The durable metadata reference implements table/column rename, column type/drop, +comments, generated-column chains and simple-view blockers. It derives generated +dependencies by binding canonical row expressions and handles bounded generated +column/view CASCADE atomically. Rename/type changes with dependents reject; SQL +definitions are not rewritten. Supported generated expressions are virtual, +top-level primitive scalar expressions with the default explicit context. This +metadata reference does not convert stored rows or enforce physical generated +values, constraints or indexes. Native 1.5 type conversion and canonical +generated-column materialization remain separately limited. All seven SDK wire +surfaces preserve these fields; unported providers reject dependency discovery. + +The added required field is part of the unpublished breaking protocol package. +The top-level method inventory stays at 84, and its schema hash is unchanged: +that hash does not cover the dependency records embedded in binary payloads. +Neither the hash nor generated codecs establish provider semantic support. + +Canonical column-expression metadata requires its selected and currently +available expression profile on CREATE, ADD and every table discovery path. +For providers that offer dependency discovery, transactional metadata mutation +also requires selected/current `vgi.transaction.ddl.v1`; omitting dependency +read from the client offer cannot bypass that requirement. Existing providers +without the dependency profile retain their legacy transaction behavior. + +### Transaction guarantees and composite writes + +**Scope correction, 2026-10-15:** reuse VGI's existing transaction and write +lifecycle. The earlier savepoint, general statement-ticket/receipt and durable +mutation recovery proposals are withdrawn from this protocol package. These +were design-only proposals; no corresponding RPCs or profiles were implemented. + +Keep `supports_transactions`, the existing begin/commit/rollback RPCs, opaque +attachment-bound transaction handles, and write init/exchange/finalize. Retain +`CatalogCapabilities.transaction` and the implemented `vgi.transaction.ddl.v1` +profile. Existing providers already implement real transactions; the task is to +qualify the DuckDB2 paths that use them, not introduce another coordinator. +Additional DML/mixed/multi-operation profiles are not selected changes: first +identify a concrete interoperability gap that the existing contract cannot cover. + +For accepted composite operations, DDL, row writes and trigger effects must use +the same provider transaction. Audit CTAS, expanded ALTER and multi-DML execution +for correct token propagation, finalization and error handling. A failed write +must not permit its partial effects to commit; use the existing whole-transaction +rollback/invalidation path. Do not add savepoints or promise preservation of +previous statements after a partially applied write fails. Preserve existing +provider-specific handling of errors that occur before effects. + +Reuse existing commit/rollback semantics, including the interface's existing +idempotency requirement. Retrying completion with the same transaction identifier +must not apply effects twice; retain normal provider results/errors. The current +transactor removes completed transactions and rejects subsequent completion calls +as inactive. That error does not establish whether an earlier COMMIT succeeded, +but it does not require a new recovery mechanism. No retained-outcome journal, +recovery endpoint or retention window is introduced. + +The earlier blanket single-attempt recommendation for transaction RPCs was too +broad. Preserve commit/rollback retry behavior; the single-attempt safeguards +apply to non-idempotent mutations. BEGIN now also uses a single attempt because +its identifier arrives in the response: the lost-response probe reproduced a +second allocation and an abandoned first transaction. Healthy subprocess pool +reuse is preserved. The original abandoned transaction remains the provider's +responsibility; the shared transactor has no timed reaper, and a client disconnect +does not reclaim it. Explicit completion or provider shutdown does. No distributed +atomicity across independent attachments is promised. + +The 2026-10-15 source audit confirms that pinned Haybarn2 already sequences +statement triggers through BEFORE/base/AFTER CTEs and wraps expanded ALTER in +its existing transaction/error machinery. VGI already passes the provider +transaction through CTAS and write finalization. Its absent engine catalog +version also makes native prepared statements conservatively rebind. These +paths do not demonstrate a need for another coordinator or a new per-write +requirements revision. Provider snapshot/concurrent-metadata behavior still +needs qualification. + +The initial checkpoints are 1.5-compatible: local transaction exception cleanup, +repeated completion and failure propagation through the current write phases. +These now have bounded runtime evidence below. The signed HTTP writable fixture's +sealed-token bug was reproduced and fixed with authenticated callback parameters; +the wire token remains sealed. The 2026-10-22 follow-up +narrows the finalization findings: current generic/simple writable fixtures +have no provider finalize callback and return results per input batch. Locked +`vgi-rpc` 0.50.0 buffers `out.emit` until `process` succeeds, so an ordinary +emit-then-raise does not send a success response followed by an error. Raw +pre-FINALIZE drain robustness is not a demonstrated current-provider failure. + +Optional final output is allowed by the table-in-out API, but native writable +finalization currently interprets it as count output; deferred RETURNING is +an adapter support gap. Add final-row handling with a concrete provider/test +before enabling that form, using the existing schema and lifecycle. Native +OLD/NEW changes adaptation belongs to the DuckDB2 lane. Exact source anchors +are in the manifest's `write_path_source_audit_2026_10_15` and +`write_finalization_source_audit_2026_10_22`. These are source findings only; +EC2 was unavailable and no new tests were run. + +The subsequent EC2 checkpoint on `18.234.138.19` ran seven native 1.5 subprocess +probes: normal INPUT RETURNING, buffered emit/process error, count finalization, +multiple batches, zero input, finalizer error, and expected rejection of deferred +RETURNING. All behaved as expected. This diagnostic provider has no transactions +or stored rows; it does not qualify rollback or signed HTTP. See +`write_finalization_runtime_checkpoint_2026_10_22` for exact evidence. + +The subsequent native transaction cleanup checkpoint reproduces the local leak +on provider completion errors (24 failing lifetime assertions before the fix). +The manager now transfers ownership before commit/rollback, and Commit retires +its schema cache before the provider call. Scoped destruction releases the local +transaction even when the call throws; bound catalog entries retain their +existing bounded catalog lifetime. Errors, RPCs, transaction identifiers and +retry policies are unchanged. No compensating rollback is introduced. + +On EC2, all 960 assertions pass across six scenarios: commit/rollback success, +provider rejection, and provider error after completion, each repeated three +times. Prepared-statement reuse, fresh transactions, a concurrent connection's +snapshot, metadata lifetime and exact callback traces pass. The existing native +semantic ALTER transaction test also passes rollback, commit and reconnect. +This diagnostic does not mutate rows or simulate transport loss. It does not +qualify duplicate completion, BEGIN allocation cleanup, signed HTTP, or broader +write atomicity by itself. See `transaction_cleanup_checkpoint_2026_10_22` for +that evidence and the subsequent checkpoint below for those follow-ups. + +The transaction/write checkpoint completes two further bounded checks on the +same pinned 1.5 engine: + +- Six lifecycle tests use real DuckDB transactions through the existing + transactor's Unix RPC server. Repeated and opposite completion calls reject an + inactive token without reapplying effects or disturbing another transaction. + Disconnect leaves an open transaction; explicit provider shutdown rolls back + its DML and DDL, verified after reopening storage. +- The native lost-BEGIN probe now returns an error after exactly one allocation. + Three healthy autocommit queries reuse one worker PID. ATTACH primes separate + concrete HTTP provider parameters for single-attempt calls; reusing that cache + avoids reentering transaction initialization during HTTP BEGIN. Those + single-attempt parameters are immutable after publication. Shared-container + dispatch honors the same retry classification, with readiness retries allowed + before dispatch; that transport change is compiled and source-reviewed only. +- Signed HTTP callbacks receive authenticated provider transaction bytes in + `BindParams`, `InitParams` and `ProcessParams`, including continuation turns. + Requests and serialized continuation state retain sealed tokens. Seven focused + pytest items cover commit/rollback, count/rows, wrong-attachment rejection and + Worker/MetaWorker lifecycle callbacks; 124 existing regression items and 74 + callback/storage items pass. No wire fields or protocol version change. +- Seventeen native signed-HTTP scenarios use a real SQLite transactional + reference: five successful count/RETURNING/rollback/zero-row/CTAS cases and + twelve INSERT/CTAS failures at initialization, the second batch and finalization + in autocommit and explicit transactions. The provider does not self-roll-back + on callback error. Native rollback removes partial writes and CTAS tables, + including an earlier successful statement when COMMIT follows the error. + Independent committed readback and exact completion traces verify the result; + failed RETURNING produces no partial successful rows, and a later write succeeds. + +The EC2 Python DuckDB 1.5.5 lacks this project's private `subcursor()` and +`enable_suspended_queries` APIs. Lifecycle tests therefore register real DuckDB +cursors directly, while write tests use the bounded SQLite provider. This proves +the tested native transaction path, not the complete custom DuckDB transactor or +every backend. No timed orphan reaper, savepoints, recovery service or changed +COMMIT/ROLLBACK retry semantics are introduced. Deferred RETURNING, native2 +OLD/NEW, triggers, expanded ALTER and multi-DML qualification remain separate. +Exact artifacts and source/binary hashes are recorded in +`transaction_write_checkpoint_2026_10_22` in the manifest. + +The October 22 full launcher regression run was non-green: 242 actual passes, +66 failures and 59 skips. Baseline source and representative probes account +for 64 constraint-negotiation failures. Two full-run scan failures pass focused +old/current Python reruns; their cause remains unresolved. Whole-repo lint and +mypy pass; one unchanged baseline file fails formatting. The manifest separates +this broader status from the passing transaction/write checkpoint. + +The subsequent October 23 checkpoint corrects native constraint negotiation: +canonical read authority permits discovery of trusted ENFORCED/ALL_ROWS facts +without granting write authority. Native constrained DML, constraint DDL and +unique-index DDL check enforcement explicitly. All four table-loading paths +check the selected read profiles, including time travel. No provider constraint +metadata is removed or downgraded to make discovery succeed. + +Native CHECK discovery accepts one numeric comparison (`=`, `<>`, `<`, `>`, +`<=`, `>=`) under `vgi.expression.duckdb.scalar.v1` in the read category. Operands +are primitive signed numeric columns (including FLOAT/DOUBLE) or numeric +constants; the complete expression record must identify the owning flat schema +and explicit default evaluation context. The receiving binder verifies the +actual table columns and Boolean result. Functions, casts, generated or +extension-typed operands, other expression shapes/contexts and extra statements +reject. This bounded read support does not enable CHECK creation or general +scalar expressions. Sixteen C++ cases (415 assertions) and four integration +probes (308 assertions, no skips) verify the boundary, including a real SQL +INSERT rejected before write discovery on the read-only fixture. + +The scan investigation also reproduces silent truncation through real launcher +workers: after an early process exit, native returned successful counts of zero +or 100 from an incomplete scan. VGI requires explicit Arrow EOS; physical EOF is +not successful completion. Raw function connections now reject incomplete reads +with an I/O error, including EOF at a valid batch boundary. Twelve launcher and +subprocess cases verify failure without partial successful output after empty, +nonempty and split batches, plus healthy empty-batch, empty-result and pagination +controls. This changes neither the wire format nor retry policy. + +The original spontaneous disconnect was not reproduced: predecessor tests and +30 stress cycles passed. A separately reproduced RPC launcher idle race has no +established link to that run and is recorded as a dependency follow-up, with no +dependency change here. The manifest's `regression_checkpoint_2026_10_23` records +the full-suite result and the distinction between injected failures and the +original intermittent observations. + +The final October 23 full launcher suite passes 308 executed cases and 11,538 +assertions with zero failures; 59 environment-dependent cases remain skipped. +Both previously failing scan tests pass in that run. Whole-repo Python lint and +mypy pass; the same unchanged baseline formatting issue remains. These results +qualify the tested configuration, not the skipped transport/backend matrix or +the DuckDB2 adapter. + +### Trigger definitions and statement ownership + +**Deferred by the October 23 combined review.** The following candidate design +preserves prior source findings for future work. Its records, methods and +profiles are not selected for the current freeze and must not be advertised. + +Reuse `CatalogObjectRef(kind="trigger", schema_path, owner_table, name)`. +Persistent definitions are worker-authoritative and survive reconnect/DETACH; +local engine entries are versioned materializations. Temporary status follows +the owning table and its lifetime, never an independent persistent-creation flag. + +`TriggerDefinition` contains the ref, `timing: BEFORE|AFTER`, singular +`event: INSERT|UPDATE|DELETE`, `granularity: STATEMENT|ROW`, +`update_columns: list`, nullable `new_table_alias`/`old_table_alias`, +`action: SqlStatement`, `execution_owner: ENGINE|BACKEND`, nullable comment and +string tags. `TriggerInfo` adds a nonempty opaque definition revision and complete +resolved dependencies. One definition has one event. Empty UPDATE paths means +any UPDATE; nonempty paths mean columns mentioned in SET, not values that +actually changed. No WHEN clause or arbitrary SQL script is introduced. + +Use `catalog_table_triggers`, `catalog_trigger_get`, `catalog_trigger_create`, +`catalog_trigger_drop`, and `catalog_trigger_alter_metadata`. They take existing +attachment/transaction context and raw object identity. CREATE takes a complete +definition plus ERROR/IGNORE/REPLACE; REPLACE atomically changes definition and +dependencies. Metadata alteration takes +`CatalogMetadataChanges` plus an expected revision. DROP has explicit +ignore-missing/CASCADE semantics. GET returns optional `TriggerInfo`; listing +returns complete infos and the existing catalog version from +one authorized snapshot. CREATE/ALTER return canonical readback. Incomplete +permission-filtered listing cannot be used as an executable trigger plan. + +The initial `vgi.trigger.duckdb.statement.v1` action profile follows the inspected +pin: one INSERT/UPDATE/DELETE/MERGE action, statement BEFORE/AFTER forms, one +trigger event, and only one-component UPDATE OF paths. A literal identifier +`"a.b"` stays one component. Transition tables are AFTER STATEMENT only, cannot +coexist with UPDATE OF, and respect event applicability (no OLD for INSERT or +NEW for DELETE). INSTEAD OF, BEFORE ROW and UPDATE ROW reject. Row and statement +triggers cannot mix for one table/event. A separate +`vgi.trigger.duckdb.row_after.v1` profile would cover only the pinned AFTER +INSERT/DELETE subset; generated source columns, row transition tables, +unsupported action forms, cascading row triggers, RETURNING and ON CONFLICT +remain rejected. Explicitly reject row UPDATE action bodies, bodies without an +applicable OLD/NEW column reference, row self-writes, and recursive trigger +cycles. Statement INSERT ON CONFLICT DO UPDATE with AFTER NEW transition tables +is also rejected by the pin. Do not silently turn a row trigger into a statement +trigger. + +The pinned ROW implementation is set-based and decorrelated: row order is +unspecified, all rows for trigger A execute before trigger B, and an individual +firing does not see writes made by earlier individual firings. A row profile +must preserve these limits rather than promise sequential per-row visibility. + +Execution ownership is exclusive for the complete table trigger plan in the +initial profile. ENGINE means the qualified client expands definitions once and +the backend does not also fire them. BACKEND means the provider owns all trigger +execution; clients never install those definitions as executable engine entries. +The initial profile rejects mixed owners rather than leave relative ordering +undefined. Listing order within each event/timing/granularity phase must match +the pinned engine's case-insensitive alphabetical order. Its `ScanTriggers` +consumer preserves the supplied order rather than sorting it; do not use +incidental hash-map order or add a separate ordering record. + +Start with ENGINE-owned execution and worker-persisted definitions. Native +`ScanTriggers` currently serves both discovery and executable trigger selection; +it cannot safely expose BACKEND-owned definitions as ordinary engine entries. +Native discovery of those definitions needs an engine filtering hook before +exposing them through `duckdb_triggers()`. RPC-only discovery can avoid that +engine change. The current transactor's per-batch SQL still does not establish +whole-statement backend trigger semantics. + +Pinned statement-trigger expansion forces base DML `RETURNING *`, including +when no transition alias is declared, so this path needs the existing rows +mode. UPDATE with OLD capture additionally needs native adaptation of the +existing paired OLD/NEW `changes` result into the engine's NEW-then-OLD columns +and its duplicate-target policy. Returned rows must not be zipped to input OLD +values without guaranteed correspondence. The existing carrier is sufficient; +no additional OLD/NEW wire record has been justified. + +For execution, audit the existing table/write discovery, sealed bind state, +transaction context, init/exchange/finalize and OLD/NEW `changes` results first. +Use those paths to carry and validate table identity, execution ownership, +required result mode and current definition revision wherever possible. A cached +binding must not bypass a newly required trigger or constraint; determine the +smallest missing validation or metadata field from the actual execution path +before selecting a new wire record. The earlier `WriteRequirements`, +`CatalogWriteBinding`, `WriteExecutionContext` and intent/ticket/receipt design +is withdrawn as a selected implementation plan. + +The remaining concrete trigger questions are execution ownership, ordering, +UPDATE OF targets, complete OLD/NEW results, zero-row behavior and errors across +multiple batches. Existing function finalization may provide the needed boundary; +prove that against the pinned engine. A batch is not necessarily the complete +original SQL statement: the current transactor issues backend SQL per batch. +Do not infer statement-trigger semantics merely from those backend statements, +and do not infer that a new coordinator is required merely because batching +exists. Add only a demonstrated missing boundary or metadata value to the +existing path, with a focused regression. If a trigger form cannot be supported +correctly yet, reject it before effects rather than add a general workflow system. + +All supported base and trigger writes use the existing transaction. Propagate +failure through the existing error/rollback path and ensure finalization errors +cannot lead to commit. Trigger/constraint changes concurrent with a write must +serialize or cause a conflict using provider transaction and revision checks. +There is no separate statement-begin/complete/abort RPC family, statement ticket, +operation receipt transport, recovery service or savepoint support in this scope. + +Proposed read/DDL profiles remain `vgi.trigger.read.v1`, +`vgi.trigger.create.v1`, `vgi.trigger.drop.v1`, `vgi.trigger.metadata.v1`. +SQL/action semantic profiles belong to the read category, as the existing +scalar-expression profile does; they do not independently grant execution. +Candidate execution profiles `vgi.trigger.engine.v1` and +`vgi.trigger.backend.v1` remain under review. Execution requires matching client +and provider behavior, transaction support and the applicable result mode. +Read/create profiles do not imply execution. None is currently advertised. + +### Mutation response failures + +Durable mutation replay/status is out of scope for this combined release. +Do not add `catalog_mutation_execute`, `catalog_operation_status`, operation +identity records, receipt journals or automatic write recovery. Ordinary errors +and the existing no-automatic-retry policy are sufficient for this work. Existing +read/query result replay functionality is unaffected by this scope decision. + +### Design acceptance and implementation order + +1. Qualify the expression/context carrier and role matrix, including typed NULL, + generated dependency cycles, old-row USING evaluation and schema metadata + preservation. Preserve the existing macro-default evaluation distinction. +2. Implement canonical constraint read/create/apply and typed index records in + a durable reference. Prove real enforcement, existing-data failure rollback, + FK NULL/action semantics, primary-key nullability and backing-index closure. +3. Continue from the native 1.5 cleanup, completion, signed-token and persisted + INSERT/CTAS rollback checkpoints. Qualify expanded ALTER, multi-DML and the + custom DuckDB transactor through the existing transaction paths. + No savepoints or general statement-ticket/receipt lifecycle is required. +4. Complete remaining column semantics and optimizer proofs from the October 23 + review. Canonical macro families, DEFAULT/USING and nullable default-schema + wire migrations are implemented. Broader macro provider/body qualification + remains separate. Trigger CRUD/execution is deferred; retain + its proof requirements above for any future scope change. +5. Extend dependency planning to constraints and column/ALTER blockers. Test + rename/replacement, inaccessible dependents, cross-schema identity and no + cross-attachment authority assumption. +6. Generate the selected records/methods across all seven SDKs, with positive + reference behavior and safe unsupported paths. Freeze only after these proofs; + then qualify the native 2.0 adapter separately. No implementation is required + to advertise every profile, but all must understand the frozen wire inventory. + +Source evidence is the pinned catalog field audit and local Haybarn 2.0 checkout +`e1784fa4231072d7152b8aaa20977bdb8ff4ace3`, especially `constraint.hpp`, +`transform_alter.cpp`, `bind_create_table.cpp`, `bind_create.cpp`, +`bind_trigger_expansion.cpp` and `table_catalog_entry.cpp`. Existing implementation +anchors are `vgi/catalog/layout.py`, `query.py`, `dependencies.py`, +`catalog_interface.py`, `vgi/protocol.py`, `vgi/write_results.py`, and the durable +schema/layout/column/query fixtures. Source inspection establishes engine limits; +it does not qualify the proposed runtime contracts. diff --git a/docs/protocol/vgi-attach-tickets.md b/docs/protocol/vgi-attach-tickets.md index 0478dc50..52b45ef6 100644 --- a/docs/protocol/vgi-attach-tickets.md +++ b/docs/protocol/vgi-attach-tickets.md @@ -1,7 +1,7 @@ # VGI Attach Tickets **Status:** Normative. Reference implementation: vgi-python `vgi/attach_ticket.py`. -**Protocol:** `vgi.attach_tickets.v1`, version `2.0.0`. No change to `vgi.v2`. +**Protocol:** `vgi.attach_tickets.v1`, version `2.0.0`. No change to `vgi.v3`. **Revision 2026-10-17 (`2.0.0`):** `AttachTicket.expires_at` changed from `float64` unix seconds (`+inf` = no expiry) to nullable `timestamp[us, tz="UTC"]` (null = no expiry). The sealed payload (§2) is @@ -136,7 +136,7 @@ from `vgi_rpc.Reflection.v1` `list_protocols`): (`VGI_RPC_GRANT_KEYS`) or the worker supplies its own `mint_grant`. A ticket is useless without a grant. -It is hosted beside `vgi.v2` and never changes how a `vgi.v2` request is +It is hosted beside `vgi.v3` and never changes how a `vgi.v3` request is dispatched. Wire name `vgi.attach_tickets.v1`, `protocol_version = "2.0.0"`. ### 5.2 Shapes @@ -204,7 +204,7 @@ A worker MAY refuse by policy with `action_denied`. ## 6. Redeeming: what `catalog_attach` does -When a `vgi.v2` `catalog_attach` request's options contain a key equal, +When a `vgi.v3` `catalog_attach` request's options contain a key equal, case-insensitively, to `vgi_attach_ticket`, the SDK framework, before any catalog code: diff --git a/docs/protocol/vgi-catalog-query-pushdown-design.md b/docs/protocol/vgi-catalog-query-pushdown-design.md index ef176745..4905ed1c 100644 --- a/docs/protocol/vgi-catalog-query-pushdown-design.md +++ b/docs/protocol/vgi-catalog-query-pushdown-design.md @@ -1,16 +1,32 @@ -# VGI 2 catalog query pushdown +# VGI catalog query pushdown -**Status:** proposed implementation design, not an implemented or normative wire contract. -**Updated:** 2026-10-06. **Original proposal:** 2026-09-13. +**Status:** engine integration design; the Python preparation/execution lifecycle and shared +SDK codecs and the opt-in native 1.5 explicit adapter are implemented on the preparation +branch. Native adapter validation is recorded separately in the manifest; automatic 2.0 +integration remains open. +**Updated:** 2026-10-25. **Original proposal:** 2026-09-13. **Scope:** optional, read-only catalog query execution; Python SDK; DuckDB 1.5 explicit-query adapter; -DuckDB 2.0-development automatic-pushdown adapter. - -The implementation target is an optional extension of the VGI 2 protocol family, proposed for -wire version **2.2.0** under the existing protocol name **`vgi.v2`**. The current Python SDK and -DuckDB extension already use `2.1.0`; this is no longer a change to an unreleased `2.0.0` contract. -New query APIs in this design remain proposals. - -| Component | Current baseline | Role in this design | +DuckDB 2.0-development automatic-pushdown adapter. The October25 bounded Python +statement profile extends the shared execution lifecycle; native entry points +and automatic pushdown remain read-only. + +The implementation belongs to the **combined successor protocol release**, alongside the +catalog/type changes in the [manifest](duckdb-2.0-manifest.json). It does not reserve a separate +minor release. Published main now uses `vgi.v2` / `2.2.0`; the prepared successor +now uses `vgi.v3` / `3.0.0`. Separate versioned endpoints/processes, draining old +sessions, and reattaching/repreparing remain the migration policy. Coordinated +release qualification is still required. See the +[latest-main integration checkpoint](duckdb-2.0-main-integration-2026-10-24.md). + +The current wire authority is [prepared read queries and typed parameters](shared-catalog-values.md#prepared-read-queries-and-typed-parameters) +and `vgi/catalog/query.py`. This earlier design's illustrative schemas/provider pseudocode are +superseded by those definitions. Typed one-row parameter batches and explicit preparation release +are included now. The finite reference qualifies lifecycle mechanics; it does not qualify a +general SQL provider or automatic DuckDB 2.0 replacement. Native `vgi_query` +qualification is a separate checkpoint against that finite reference. The commit table +below records the original design survey, not current implementation heads. + +| Component | Historical survey baseline | Role in the original design | |---|---|---| | `vgi-python` | `2029f09`, package `0.42.1`, wire `2.1.0` | Worker, protocol, client, and generators | | `vgi` | `de68c17`, wire `2.1.0` | Existing extension and reusable execution machinery | @@ -21,6 +37,226 @@ New query APIs in this design remain proposals. “DuckDB 2.0” means the inspected development API, not a released upstream compatibility claim. Wire versions, SDK package versions, and the two engine targets are separate version axes. +## Explicit statement reuse audit — October 25, 2026 + +The user reopened bounded explicit remote DML/DDL and retained the named-type / +ENUM deferral. The normative extension is [explicit transactional statements](shared-catalog-values.md#explicit-transactional-statements--october-25-2026). +It reuses preparation/release, typed parameters, transaction handles and the +existing TABLE stream. One profile/mode pair and fixed result-schema metadata +are sufficient for the bounded proof; there is no new RPC or record field. +The read-only scope of automatic whole-query pushdown remains unchanged. + +The source audit used native `6476b97473ff`, its retained 1.5 engine +`105edd31b57f`, and local Haybarn2 `74a0e809885f`. The latter is newer than the +October 24 audited `e1784fa42310` pin and is not a runtime-qualified VGI engine. + +| Finding | Evidence and consequence | +| --- | --- | +| Transport and ownership already exist | `vgi/query_execution.py` and `vgi/query_provider.py` already route prepare, resolve, start, read, close and release with attachment/transaction ownership. Keep those methods. | +| Current explicit contract is read-only | `vgi/catalog/query.py` and native `src/vgi_query_rpc.cpp` select fixed profile/mode pairs. Add optional statement semantics rather than reinterpret readonly. | +| Result layout is fixed before effects | Every batch is checked against prepared schema including metadata. Put result kind in that schema and command counts in its row. Read DDL versions through existing catalog RPCs. | +| TABLE scans can disappear | Native `src/vgi_query_function.cpp` executes only at init; Haybarn2 `pushdown_limit.cpp`, `pushdown_filter.cpp` and unused SELECT CTE handling can eliminate its GET. Disabling filter/projection pushdown does not establish mutation execution. | +| Transaction transport is sufficient | The native binder already obtains `VgiTransaction`; normal commit/rollback participation exists. A mutation route still needs modified-database registration, valid-transaction checks and provider completion enforcement. | +| DDL needs local cache clearing | Native `VgiCatalog::DropSchema` clears transaction/schema caches; explicit query execution does not. A new-transaction version check alone cannot refresh subsequent lookups within the current transaction. | +| CALL / RemoteExecute alone are insufficient | The 1.5 CALL binder rewrites to SELECT. Haybarn2 `Catalog::RemoteExecute` returns TableRef and the remote optimizer wraps it in SELECT (`remote_pushdown_optimizer.cpp:1842`). Neither source fact qualifies safe mutation execution. | + +Keep native `vgi_query` read-only. A possible later route is a dedicated top-level +extension statement with a private executor: 1.5's `ParserExtensionPlanResult` +already has `modified_databases`, `requires_valid_transaction` and `return_type`. +It must keep preparation effect-free, fully drain/validate execution before +success, preserve existing rollback/error behavior and clear caches. This is a +source-backed candidate, not an implemented or tested native entry point. + +The finite Python proof uses real transaction-owned DuckDB connections and the +ordinary public signed-HTTP RPC/client path. It must demonstrate DML counts, +RETURNING, transactional DDL/readback, commit/rollback, incomplete/failed commit +rejection, cancellation and no automatic retry after ambiguous init. The fixture +is single-process and finite; it does not qualify distributed connection routing, +active-SQL cancellation, a general SQL sandbox or native execution. Final EC2 validation passes 4,646 Python tests (129 skips), all 18 existing +native read-query scenarios, strict generated-artifact checks for every SDK, +Ruff and focused mypy. The separate-process localhost HTTP example also passes. +Only Python implements the statement profile; the other SDK changes update +generated service descriptions. Evidence is recorded in +`explicit_statement_checkpoint_2026_10_25`. + +## Implementation checkpoint — October 24, 2026 + +The source-review table and original A0 counts below predate the latest-main merge. They +remain historical inputs. Current merged refs, 1.5 runtime evidence and the refreshed 2.0 +compile inventory are recorded in the [integration checkpoint](duckdb-2.0-main-integration-2026-10-24.md). +The refreshed inventory attempted all 110 native C++ sources: 54 compiled and 56 failed; +generated `describe.c` also compiled. The merge changes the published baseline to 2.2.0 and +preserves the successor work; it does not add automatic whole-query execution. + +**Automatic whole-query pushdown is not implemented.** The protocol and explicit execution +machinery are reusable; the substantial remaining pieces are the native 2.0 read port, the +optimizer adapter, and a provider that proves equivalent SQL execution under the same data +snapshot. This source review found no demonstrated need for another wire change. + +This checkpoint supersedes the old seven-PR sequence as the next-work list. Sections 1–13 +remain the detailed design, subject to the narrower first milestone below. Review inputs: + +| Component | Reviewed revision | Meaning | +| --- | --- | --- | +| Native VGI | `38fe3c775bd332b9cd4b845f3518631ee6070955` | Working 1.5 explicit adapter and bounded scalar projection/repeatability preparation | +| Python VGI | `0566afd7f12a93dc8441db930ae85d764d35b570` | Portable query records, execution ownership and finite reference | +| Native engine | `105edd31b57fe7e6d32ee648efad562e45a9f908` | Retained 1.5 build; measured shell banner `v1.5.6-dev149` | +| Audited 2.0 engine | `e1784fa4231072d7152b8aaa20977bdb8ff4ace3` | Source for the exporter, catalog and optimizer findings below | +| Haybarn `next-2.0` HEAD at review | `5ab8a76db12ad1ad63c4e4997bce37602a9d0b43` | Later tree, not substituted for the audit pin; choose and reconcile the implementation pin in A0 | + +### What is already available + +- Query capability/profile negotiation, preparation and release, typed Arrow parameters, + transaction/context and resolved-object manifests, expected output schema and ordering. +- Owner-bound preparation/execution handles and the existing TABLE bind/init/read/cancel path. + The native 1.5 `vgi_query(...)` adapter has 18 finite-reference scenarios; shared codecs, + signed-HTTP fragment continuation and cross-worker ownership have separate bounded tests. +- Bounded scalar projection and per-bind repeatability. These help ordinary scans but do not + implement whole-query export or prove that two independent binds share a data snapshot. + +The reference provider accepts six exact SQL strings over an immutable seven-row table. It +creates fresh DuckDB connections for preparation and execution, materializes results before +returning init, and checks result-byte limits afterward. Its interruption test delays fragment +reading after materialization. Those tests establish useful lifecycle behavior, not general SQL +preparation, mutable snapshot equivalence, incremental quotas or cancellation of active SQL. + +### First user-visible milestone + +Execute ordinary SQL, without an explicit `vgi_query` call: + +```sql +SELECT c.region, count(*) AS paid_orders +FROM warehouse.main.orders AS o +JOIN warehouse.main.customers AS c ON c.id = o.customer_id +WHERE o.status = 'paid' +GROUP BY c.region +ORDER BY c.region ASC NULLS LAST; +``` + +Both tables must belong to one exact attachment and existing transaction. A successful +demonstration must show one remote query execution, no ordinary table-scan execution, and only +the final grouped rows crossing the result stream. Compare rows, multiplicity, names, types and +ordering with local execution under the same backend snapshot. A valid provider decline must +retain the ordinary local join/aggregate; absent support must make no preparation RPC. Trace +preparation separately from execution: metadata lookup/binding can still contact the worker. + +Start with whole read-only SELECT roots, INNER equijoins, deterministic predicates/projections, +GROUP BY and COUNT over certified primitive types. Add MIN/MAX and exact decimal SUM only as +their type/overflow/NULL cases pass. Use explicit ordering; leave unordered LIMIT and +insertion-order-dependent shapes local. This is an allowlist, not permission to export every +operator accepted by DuckDB's SQL exporter. + +Initially decline mixed attachments/local tables, subplan extraction, views, generated or +delegated tables, required-filter policy markers, multi-branch scans, sampling/time travel, +volatile or unqualified functions, external access, complex CTEs/subqueries/windows and +unqualified nested types. Preserve raw schema/table components even in the flat-schema pilot. +Broader coverage is subsequent adapter qualification, not another planned protocol release. + +**Parameterized automatic queries are a follow-up milestone.** At the audited pin, +`bound_expression_sql_exporter.cpp` rejects `BOUND_PARAMETER`. Use the existing zero-parameter +representation for the first milestone and retain parameterized plans locally. Later work must +preserve typed placeholders, exact mapping and fresh preparation on EXECUTE; never interpolate +parameter values into SQL. Explicit `vgi_query` already supports typed parameters. + +### Remaining work and reviewable checkpoints + +**Historical pre-main A0 inventory.** The isolated configuration at audited engine +`e1784fa42310` passes. All 107 native C++ sources were attempted: 53 compiled and 54 failed; +generated `describe.c` also compiled. Shared catalog/index declarations are the first blocker +in 27 failed sources. See the native `docs/duckdb-2.0-a0-diagnostics.md` and JSON inventory. +There is no engine/loadable link or runtime proof yet. The next A1 batch starts with shared +catalog/read declarations, then bound functions and read conversion. Source counts are not a +migration-completion percentage; the overall effort estimate remains provisional. + +**Enablement is worker opt-in through attachment capability discovery.** The client offers +its supported encoding/profiles and actual engine build in `ClientQueryCapabilities`; the worker +selects the compatible subset in `QueryCapabilities`. Automatic support requires the selected +`automatic` mode and `vgi.duckdb.relational.v1` profile, with transaction support. A worker may +omit query support entirely or select explicit queries only. Neither case enables automatic +pushdown, and neither requires probing it with preparation RPCs. + +Once the 2.0 adapter is qualified, ordinary eligible queries use the discovered support +without a user setting. Capability discovery is the enablement mechanism; there is no automatic +pushdown on/off/require control. Each candidate still requires eligibility checks and provider +acceptance during preparation. The existing records already express this agreement; no new +enablement field is needed. The current 1.5 explicit-query setting does not gate this future +2.0 automatic path. + +Effort ranges below are engineering effort, not measured execution time or delivery promises. +They assume one experienced engineer, one DuckDB-backed provider, Linux ARM64, and reuse of the +current wire and execution lifecycle. A0 must narrow the largest uncertainty before treating the +rest as a schedule. Each checkpoint should land independently with its evidence and remaining +failures recorded; do not wait for the full feature to report progress. + +| Checkpoint | Work and exit evidence | Dependencies | Indicative effort | +| --- | --- | --- | --- | +| A0: pin and diagnostic build | Initial checkpoint recorded at audited pin: configuration passes, all 107 native C++ sources attempted (53 compiled / 54 failed). Keep full source compilation for diagnostics; first runtime scope remains reads. Linking is blocked by compile failures; 1.5 is unchanged. | None | Initial checkpoint reached; original budget 1–3 engineer-days | +| A1: working 2.0 read path | Port identifiers/qualified names, optional default schema, table column ownership, bound table functions, typed plan bindings and Arrow cardinality. Demonstrate ATTACH, flat table lookup, an ordinary scan and explicit `vgi_query`. | A0 | 4–10 days | +| A2: real query provider | Prepare a bounded family of SQL SELECTs against actual logical catalog tables. Reuse the existing transaction owner so ordinary scans and query execution share a real backend snapshot; prove concurrent mutation and prior-write visibility where supported. Use bounded fragment spooling and active-query interruption. An initial explicit-provider proof can run on 1.5. | Can start alongside A0/A1; certify against the chosen engine before A5 | 4–8 days | +| A3: eligibility and export | Add the early bound-plan hook before existing VGI projection/multi-scan transforms. Export an eligible whole SELECT; retain source identities and an exact positional output map. Demonstrate unsupported export leaves local execution intact. | A1; can use a declining provider before A2 is complete | 3–6 days | +| A4: preparation and replacement | Extend native explicit-only capability/request/digest handling to automatic mode. Discover the worker's selection at ATTACH without requiring the 1.5 explicit-query manual enablement step. Populate transaction, metadata revision, table manifest, expected output and ordering. Build and verify the replacement before swapping; reuse the reserved executor, preparation lease and single-attempt execution. Show the ordinary SQL example succeeds and a clean decline remains local. | A2 + A3 | 4–8 days | +| A5: bounded pilot qualification | Differential types/NULLs/duplicates/empty inputs/order; snapshot and logical-object authorization; real plan copying and prepared re-execution; active cancellation, quotas and cleanup. Test subprocess first, then the supported HTTP path. Add EXPLAIN/decline diagnostics and a rows/bytes comparison. Prove worker opt-in/discovery, absent/explicit-only support and per-query decline. | A4 | 5–10 days | + +The table is roughly **21–45 engineer-days** for the bounded qualified pilot, with uncertainty +at the upper end from the engine port and provider ownership. Budget approximately **2–5 +engineer-weeks for a first end-to-end demonstration** and **5–10 engineer-weeks for a qualified +pilot**, including contingency. The demonstration is an intermediate result inside A2–A4, +not an additional work package. These are judgment ranges from source review, not measured +remaining work. Provider work and the native port can proceed in parallel; this can reduce +elapsed time without removing the work. Re-estimate immediately after A0/A1. + +This estimate excludes automatic parameter support, arbitrary relational coverage, all-SDK +backend implementations, WASM/all-platform distribution and the full native M0–M4 migration. +The monolithic native source list currently includes writes, COPY and advanced operators: A1 +must mechanically adapt affected compilation units or explicitly omit unsupported modules in +the isolated target. A read-only pilot does not make those compile dependencies disappear, and +omitting them must not advertise their availability. + +### Technical choices and acceptance boundaries + +- Use `OptimizerExtension::pre_optimize_function` and ordinary + `LogicalPlanSQLExporter::Export`, reconstructing approved logical VGI tables through + `GetTable()` or a small pure `to_sql` callback. At the audit pin `ExportWithSources` inserts + MATERIALIZED CTEs for replacements; reserve it for a demonstrated need after checking its + impact on backend optimization. No new parsed `RemoteExecute` hook is on the critical path. +- Register whole-query consideration before scalar projection and multi-scan transforms. + A decline continues through normal local optimization. Keep parsed `EXECUTE_QUERY_NODE`, + `EXECUTE_STATEMENT` and `CONNECT` delegation disabled. +- Keep the original logical plan until the replacement and its result mapping are verified. + Match every positional binding, semantic type and required schema metadata; initially decline + semantic/optimizer type divergence. Preserve local labels separately from unique transport + names. Invalid plans, authorization failures, malformed responses and backend/transport errors + propagate; only unsupported shapes and valid preparation declines stay local. Never replay + locally after remote execution starts. +- Metadata versions/etags are not data snapshots, and `STABLE_WITHIN_QUERY` is a claim about one + exact bound state, not independent tables. A2 must prove shared data visibility using the + existing transaction mechanism. Do not add savepoints, replay/status services, another query + coordinator or a separate snapshot registry merely to achieve this proof. +- Keep the synthetic executor nonrepeatable/nonexportable, with one ordered reader, no result + caching, scan pushdowns or automatic retry. In the audited engine a nonnull `GetTable()` can + establish repeatability before the callback is consulted; do not attach a catalog table to + this synthetic executor merely to make SQL reconstruction work. Generalize the current + explicit-query execution guards so automatic execution retains them. +- Local EXPLAIN/preparation must not execute SQL. Prove one init after acceptance, correct + preparation release, active-query cancellation, incremental byte/time bounds, abandoned-owner + cleanup and no execution replay after a lost response. Reuse existing lifetime mechanisms. +- Automatic parameters require a separate exporter integration proof. Test no-parameter plan + copying/prepared freshness first and parameterized local fallback; typed parameter support in + the wire is not evidence that the bound exporter supports it. + +Native evidence is in `src/vgi_query_rpc.cpp`, `src/vgi_query_function.cpp`, +`src/include/storage/vgi_catalog.hpp` and `CMakeLists.txt`. Provider evidence is in +`vgi/catalog/query.py`, `vgi/query_execution.py` and `tests/catalog/query_reference.py`. +Engine evidence at the exact audit pin is in +`src/include/duckdb/planner/logical_plan_sql_exporter.hpp`, +`src/planner/sql_export/{logical_plan_sql_exporter,table_function_sql_export,bound_expression_sql_exporter}.cpp`, +`src/planner/logical_operator_repeatability.cpp` and `src/optimizer/optimizer.cpp`. + +This is planning/source evidence only; it adds no runtime qualification. Existing release gates +remain open, including the Python outer wrapped-IPC framing gap and full provider/native 2.0 +qualification. Completing this pilot does not freeze the combined successor protocol or complete +the broader catalog/write/platform port. + ## 1. Decision Add a negotiated **catalog-query preparation RPC**, then execute accepted queries through VGI's @@ -55,7 +291,7 @@ while using the bound-plan path. | Logical-plan SQL exporter and structured issues | Reuse the exporter in a pre-optimizer feasibility spike; do not build a general SQL decompiler | | `BoundTableFunction`, explicit parser contexts, ref-counted `LogicalType` | Add a dedicated 2.0 compatibility layer before enabling automatic queries | | `catalog_contents`, metadata etags, version adoption | Reuse existing metadata caches; distinguish committed metadata from transaction/data snapshots | -| `vgi.v2` routing and wire `2.1.0` | Route new RPCs with generated protocol identifiers; propose coordinated `2.2.0` rollout | +| Historical `vgi.v2` routing and wire `2.1.0` | Route new RPCs with generated identifiers; release once with the combined successor transition | | Typed nested catalog records and six worker SDKs | Use typed structs for new query records and include C# schema/type generation | | Worker-batch validation, cancellation dispatcher, scan interrupt checks | Extend existing guards and lifecycle hooks rather than recreate them | | Credential-aware caches and HTTP grants | Preserve current identity isolation; keep query-result caching disabled | @@ -94,7 +330,38 @@ FROM vgi_query('warehouse', ' ``` Here the outer query runs locally and the SQL string uses the attached provider's namespace. -This syntax is an implementation target, not an existing registered function. +The development adapter registers this function on the pinned DuckDB 1.5 build. +Enable negotiation with `SET vgi_query_enabled=true` **before** attaching the +provider. A provider must select explicit query support; enabling the setting +alone does not confer support. Disabling it also prevents subsequent execution. + +Named parameters use a typed struct. Positional parameters use a struct or +`row(...)` with an explicit mapping mode: + +```sql +SELECT * FROM vgi_query('warehouse', 'SELECT $value AS result', + parameters := {'value': NULL::INTEGER}); +SELECT * FROM vgi_query('warehouse', 'SELECT $1 AS result', + parameters := row(42::INTEGER), + parameter_mode := 'positional'); +``` + +SQL text uses the provider's negotiated default evaluation context. Parameter +values travel in Arrow IPC and never become SQL text. Untyped NULLs require a +cast; changing parameter types requires fresh preparation. DuckDB prepared +statements always rebind this function, so executions use current values and +the current transaction identity. Output schema and metadata remain exact, +including empty output. The adapter uses one ordered reader, disables result +caching and scan pushdown, and never retries ambiguous preparation or +initialization failures. +Preparation release follows successful init; bind-only plans release on disposal. +Cancellation is enabled for this executor even when ordinary scan cancellation +is disabled. The provider still owns expiry/janitor cleanup after lost clients. + +`make test_queries` runs the finite reference through the exact built native +shell and a direct connection-interruption test, then inspects durable +execution/resource records. It does not qualify +arbitrary SQL providers, every transport/platform, or native DuckDB 2.0. ```text Original SQL @@ -108,14 +375,14 @@ Original SQL ## 2. What the current code provides, and what it does not -| Layer | Available now | Required addition | +| Layer | Available now | Remaining addition or proof | |---|---|---| | DuckDB development API | Consuming remote-catalog hooks; bound-plan SQL exporter; pre/post optimizer extensions | Early export feasibility, VGI source reconstruction, and accepted-plan replacement | -| VGI attachment | Client capabilities, attachment/transaction tokens, bulk catalog contents and etags | Query capability negotiation and transaction-aware metadata validation | -| VGI function execution | Dynamic bind schema, TABLE streaming, batch/type validation, cancellation dispatcher | Reserved prepared-query dispatch and stricter query execution invariants | -| Python worker | Declarative catalogs, scoped functions, state codecs, storage abstractions | Query-provider interface and transport-safe execution state | -| Python transactor | Attachment-scoped databases, transaction cursors, Arrow scan producers | Reusable query-owner integration, prepare-only metadata, explicit handle lifecycle | -| DuckDB VGI extension | Catalogs, Arrow scans, transaction integration, filter pushdown, result cache | Query binder, query-specific bind data, semantic adapter, cache exclusion | +| VGI attachment | Query capability records, attachment/transaction tokens, bulk catalog contents and etags | Native automatic negotiation and shared data-snapshot proof | +| VGI function execution | Reserved prepared-query dispatch, exact schema checks, TABLE streaming and cancellation | Automatic executor integration; active backend cancellation and incremental resource bounds | +| Python worker | Query-provider interface, typed parameters, owner-bound preparation/execution and transport-safe fragments | Real SQL provider and automatic semantic qualification | +| Python transactor | Attachment-scoped databases, transaction cursors, Arrow scan producers | Proven query-owner integration sharing the actual scan transaction; stock DuckDB lacks the fork's `subcursor()` | +| DuckDB VGI extension | Explicit query binder/scan, preparation lease, fixed reader, cache exclusion and single-attempt execution | Native 2.0 port, automatic eligibility/export, context/manifest handling and verified replacement | Important limits of DuckDB's existing **parsed-statement remote-catalog pass** (distinct from the proposed bound-plan adapter): @@ -165,7 +432,7 @@ No cross-attachment joins, upload of local temporary tables, distributed shuffle join placement, arbitrary mixed-query subtree extraction, or serialization of DuckDB internal AST/logical-plan objects. No new Substrait implementation is required. -No automatic pushdown of parameters, correlated/LATERAL queries, recursive CTEs, windows, +The first automatic adapter excludes bound parameters, correlated/LATERAL queries, recursive CTEs, windows, sampling, time travel, opaque local macro/UDF dependencies, external table functions, or queries whose order depends on an unproven physical insertion order. These can be added as independently tested profile revisions. A remote provider's broader explicit-query language must not silently broaden the @@ -195,17 +462,19 @@ them. A provider advertises automatic support only for tested client versions, f implementations, type mappings, and supported session contexts. Unknown combinations decline. Changes to that compatibility policy produce a new capability revision. -## 5. Proposed wire contract +## 5. Original wire design sketch -The following records are proposed schema definitions, not currently generated dataclasses. -Use the existing Arrow single-record IPC envelope, with **typed nested structs** for new query +**Historical illustration, superseded by the implemented records in `vgi/catalog/query.py` +and [shared catalog values](shared-catalog-values.md#prepared-read-queries-and-typed-parameters).** +The generated query models already exist; the sketches below are not a second wire authority. +Use the existing Arrow single-record IPC envelope, with **typed nested structs** for query records, following `CatalogContentsResponse.schemas`. Do not add a separate IPC stream per object reference or evaluation-context record. Existing attachment/transaction tokens remain binary; output/object schemas use Arrow IPC schema serialization, not an IPC table. Null and empty are distinct. The existing outer `CatalogAttachRequest.client_capabilities` IPC encoding is unchanged. -Route preparation, bind, and init through `VGI_MAIN_PROTOCOL` with generated name `vgi.v2` and the -selected surface version. HTTP preparation uses `{base}/vgi.v2/catalog_query_prepare`; raw +Route preparation, bind, and init through `VGI_MAIN_PROTOCOL` with generated name `vgi.v3` and the +selected surface version. HTTP preparation uses `{base}/vgi.v3/catalog_query_prepare`; raw transports carry `vgi_rpc.protocol`. The SDK-owned executor name is a function argument to normal `bind`/`init`, not a server-level reserved RPC: the `__transport_options__` routing exception must not apply to it. Register the new method in request/response schema validation and tracing. @@ -493,8 +762,10 @@ authorization decisions, result cursors, or data across principals. Schema paths are component lists throughout resolution. A component containing a dot is not two schemas. The current DuckDB 1.5 adapter's depth-one bridge must not flatten a deeper worker path. -Do not silently change the existing attachment `default_schema: str` contract in this feature; -automatic SQL uses resolved object paths and explicit mode uses the provider's declared namespace. +The combined successor already makes attachment `default_schema` nullable: a present value is +one raw top-level identifier, and NULL means no implicit default. Automatic SQL uses resolved +object paths; explicit mode uses the provider's declared namespace. No new default-schema wire +change is needed for query pushdown. ### 6.3 Output, ordering, and settings @@ -569,7 +840,8 @@ Perform ordinary local optimization afterward whether the candidate was retained Ensure that later passes cannot duplicate a nonrepeatable remote execution or push forbidden hints into the executor. -The **first technical gate** is a compiling prototype against `80e17fc252` proving export of a +The **first technical gate** is a compiling prototype against the exact 2.0 pin selected in A0 +(source audit: `e1784fa42310`, reconciled with any later pin) proving export of a VGI join+aggregate at this early stage, output/type/binding preservation, snapshot identity, and successful normal optimization after a decline. The exporter accepts planned trees but does not select a safe stage for its caller; its README explicitly calls distributed fragment @@ -593,8 +865,8 @@ unacceptable. ### 7.3 VGI adapter responsibilities -The preferred callback identifies attached VGI scans directly and checks the current session's -pushdown setting before making preparation RPCs. It leaves VGI's parsed-statement +The preferred callback identifies attached VGI scans directly and checks the attachment's +negotiated automatic query capability before making preparation RPCs. It leaves VGI's parsed-statement `EXECUTE_QUERY_NODE`, `EXECUTE_STATEMENT`, and `CONNECT` delegation disabled. Do not store per-session settings in shared catalog state. Adding INSERT/CTAS source pushdown later is a separate read-source optimization; it must not imply atomic cross-database writes. @@ -662,15 +934,15 @@ Keep this separate from in-process copies of prepared statements. The commit point is installation of the accepted replacement, not receipt of the first result batch. Eligibility/export misses and explicit worker declines before that point keep execution -local; actual query/protocol/backend errors do not authorize fallback. Repreparing -an expired receipt is bounded, must preserve the already bound output/context/transaction, and -does not mean executing the original query locally after failure. +local; actual query/protocol/backend errors do not authorize fallback. Any future support for +repreparing an expired receipt must preserve the already bound output/context/transaction; +automatic expiry recovery is not required for the initial pilot. -Bind/preparation are repeatable because they do not execute queries; existing reconnect-and-bind -logic can be reused only under that invariant. Starting a query is a separate operation. The -current `InitRequest.execution_id` distinguishes secondary initialization and must not be -repurposed as a new client idempotency key. If reliable resumable execution is added later, give -it an explicit execution-attempt and batch-sequence contract. +Preparation and bind must not execute SQL, but they can allocate owned state. Keep the existing +query adapter's single-attempt policy for preparation, bind and init; a lost response does not +authorize reconnect-and-repeat. The current `InitRequest.execution_id` distinguishes secondary +initialization and is not a new client idempotency key. Continue using existing owner cleanup +and stream continuation; no resumable execution/replay framework is part of this milestone. ## 8. Python provider and worker implementation @@ -701,8 +973,9 @@ provider should decline such objects until it can reproduce their existing contr `PreparedQuery` contains a portable preparation recipe and output metadata, not a live result cursor. Recipes may be self-contained sealed data or identifiers into shared, bounded TTL -storage. Large SQL recipes will need storage because token size is capped. No new release RPC -is necessary for recipes: abandoned EXPLAIN/PREPARE attempts expire without executing anything. +storage. Large SQL recipes need storage because token size is capped. The combined contract +includes `catalog_query_release` for prompt idempotent cleanup; abandoned EXPLAIN/PREPARE +attempts must still expire without executing anything. Receipt expiry gates starting a new execution. It does not interrupt an already accepted running execution, whose owner handle has its own deadline and idle lease. Transaction termination, @@ -856,22 +1129,22 @@ capability misses. ## 10. Planning policy and observability -Propose a `vgi_query_pushdown` engine setting with `off`, `auto`, and `require` values. Initially -default to `off` while compatibility tests and a reference provider mature. `auto` tries only -the tested subset and otherwise keeps normal VGI scans. `require` is a development/diagnostic -mode: a detected single-VGI-catalog candidate must be accepted or produce an actionable error; -it is not an instruction to push writes or mixed-catalog queries. Queries with no such candidate -are unaffected. Explicit `vgi_query` is an intentional separate entry point. +Worker opt-in is negotiated during ATTACH using the existing query capability records. The +client offers supported profiles and its engine build; the worker selects compatible automatic +support. The qualified 2.0 adapter then considers eligible queries automatically. Absent or +explicit-only support keeps normal VGI scans without an automatic preparation RPC. Individual +queries can still be declined during preparation, leaving the original plan local. -Declare this session setting in `vgi/src/vgi_settings.json` and regenerate with -`scripts/generate_settings.py`, using the existing flat-settings validation and scope rules. -Do not add an unvalidated registration or a second stored setting on `VgiCatalog`. +There is no proposed user/session pushdown setting or off/auto/require control. Do not add one +to the generated settings registry. The current 1.5 explicit adapter's `vgi_query_enabled=true` +requirement remains specific to that existing path; automatic capability discovery must not +depend on it. Explicit `vgi_query` remains a separate entry point. The first automatic policy is capability/shape based, not cost based. Prefer eligible joins, aggregations, DISTINCT once supported, and ordered reductions; skip trivial scans. Keep estimates optional and explicitly advisory. An early bound-plan extension is not a full distributed cost model, and incomparable local/backend cost units should not decide correctness-sensitive rewrites. -Benchmark before enabling automatic mode by default for a certified provider. +Benchmark the qualified matrix before releasing capability-driven automatic behavior. Local EXPLAIN may prepare but must not execute the remote SELECT. Show a `VGI_REMOTE_QUERY` node with provider/profile, redacted operation summary, result columns, order guarantee, and optional @@ -881,19 +1154,21 @@ debug diagnostics without flooding ordinary successful query logs. Useful counters: candidates, local ineligibility by bounded reason, worker declines by bounded reason, accepted preparations, started/completed/cancelled/failed executions, schema violations, -token expiry, active owners, and transferred rows/bytes. Compare actual traffic with pushdown -disabled; do not invent a “rows avoided” metric when the baseline is unknown. +token expiry, active owners, and transferred rows/bytes. Compare actual traffic against a local-execution test fixture; do not invent a “rows avoided” metric when the baseline is unknown. ## 11. Implementation map and sequencing -Start two tracks: prove the early bound-plan exporter against the current 2.0 headers, and define -the portable protocol/execution path. The first usable release remains explicit queries on 1.5; -automatic mode depends on the exporter/snapshot/policy gate, not on optimistic API assumptions. -The seven implementation units below are milestones, not claims that any query API is already built. +The October 24 A0–A5 checkpoints above are the active remaining-work sequence. The seven units +below retain the original design decomposition, with their current implementation status. +Portable query records, codecs and native 1.5 explicit execution already exist. Automatic mode +depends on a working 2.0 read path and the exporter/snapshot/policy gate. ### PR 1: contract and executable corpus -Pin the `vgi.v2` / proposed `2.2.0` contract and turn the reviewed wire portion into a normative +**Status:** portable contract and bounded lifecycle corpus implemented; automatic semantic +certification remains open. The current wire authority is `shared-catalog-values.md`. + +Review the query contract within the combined successor release and maintain its normative optional-extension specification. Specify typed nested records, metadata version-0/etag rules, and the initial automatic snapshot requirement. Add accepted and declined fixtures, exact Arrow schemas, malformed payloads, capability intersection cases, @@ -903,6 +1178,9 @@ accept, decline, and return deliberately malformed responses without executing a ### PR 2: Python protocol, dispatcher, and generated schemas +**Status:** implemented on the preparation branch. The file list below describes that original +work; do not create a second query protocol or repeat SDK generation without a concrete change. + Primary files: `vgi/protocol.py`, `vgi/catalog/catalog_interface.py`, `vgi/catalog/descriptors.py`, `vgi/worker.py`, `vgi/invocation.py` where necessary, and a new `vgi/catalog/query.py` module. Add capability/prepare dataclasses, RPC method, provider interface, reserved name protection, @@ -911,7 +1189,7 @@ ordinary functions. Update `vgi/codegen/_common.py`'s inventories and generated protocol/schema artifacts for C++, TypeScript, Rust, Go, Java, and C#. Regenerate the existing typed model/builders as well as schemas, -protocol versions, and protocol-name artifacts; preserve the routing name `vgi.v2`. Extend +protocol versions, and protocol-name artifacts; use the successor routing name `vgi.v3`. Extend inventory, nested-schema/type parity, protocol-name/version, catalog-auth-binding, and client-catalog tests. Add a Python client query convenience method that uses the same preparation and TABLE path; do not bypass the public client lifecycle with a private standalone HTTP call. @@ -919,9 +1197,14 @@ do not bypass the public client lifecycle with a private standalone HTTP call. Use `scripts/regen_generated.py` for the coordinated generation workflow rather than redirecting individual generators over checked-in files. Include `tests/test_generated_cpp_protocol_name.py` and `tests/test_generated_csharp.py` alongside the existing schema/version drift tests. +For the coordinated release, use `scripts/regen_generated.py --check --require-all` +without `--only`; this fails if any SDK checkout or generated artifact is missing. ### PR 3: reference provider and execution ownership +**Status:** ownership/fragments and a finite explicit reference implemented. A2/A5 supply the +missing real SQL provider, shared mutable snapshot and active-execution qualification. + Implement bounded preparation storage, serializable execution handles, owner routing, backend interrupt/cleanup, and transaction reuse, starting from `vgi/transactor/server.py` and its transaction-cursor model where it fits. Any private transactor protocol/client extensions remain @@ -932,6 +1215,10 @@ after differential conformance against the actual client engine builds. ### PR 4: DuckDB 1.5 explicit adapter +**Status:** implemented with 18 finite-reference scenarios. Current query-specific code lives in +`src/vgi_query_rpc.cpp` and `src/vgi_query_function.cpp`; this is reusable execution machinery. +Preparation/init use single attempts, not reconnect-and-reexecute recovery. + In the `vgi` repository, extend capability construction/parsing in `src/vgi_rpc_types.cpp` and `src/include/vgi_catalog_metadata.hpp`, with attachment state in `src/include/storage/vgi_catalog.hpp` / the storage implementation. Route the preparation method @@ -951,8 +1238,8 @@ optimizer extension into 1.5 as part of this milestone. ### PR 5: DuckDB 2.0 compatibility and export gate -Run the feasibility spike at the start, then land the proven adapter foundations here. Compile -against `duckdb-3/duckdb` at `80e17fc252`: update the declaration/bound-function split, signature +**Status:** open; A0/A1/A3. Run the feasibility spike, then land the proven adapter foundations. +Compile against the exact A0 pin, beginning from audit `e1784fa42310`: update the declaration/bound-function split, signature and argument APIs, explicit parser contexts, type ownership, and serialization callbacks in an isolated 2.0 compatibility layer. Add VGI table-source SQL reconstruction and structured export diagnostics. Leave the reserved executor nonrepeatable and nonexportable by default. @@ -968,6 +1255,9 @@ means a core change is no longer the assumed critical path. ### PR 6: DuckDB 2.0 automatic adapter +**Status:** open; A3/A4. Native query capability parsing, request construction and context digest +currently select explicit mode only; portable automatic records alone do not enable this adapter. + Add a separate version-specific adapter, for example `src/storage/vgi_remote_query.cpp` plus its header. Register the early optimizer callback and implement bound-source eligibility, SQL export, canonical name/type/context validation, prepare @@ -978,10 +1268,15 @@ the portable VGI wire layer and the 1.5 build. Do not enable the consuming remot ### PR 7: hardening, documentation, and staged enablement +**Status:** bounded explicit lifecycle evidence exists; A5 and wider automatic coverage remain +open. Workers opt in through capability discovery; claim only the tested engine/provider/operator +matrix, with no additional user enablement required once the adapter is qualified. + Run semantic, cross-process, transaction, security, and fault-injection suites. Document the -provider contract, opt-in catalog example, `vgi_query`, EXPLAIN, decline reasons, and rollout -settings. Publish compatibility claims for tested engine/provider combinations, not just a -minimum semver. Enable `auto` by default only for certified providers after performance evidence. +provider contract, opt-in catalog example, `vgi_query`, EXPLAIN, decline reasons, and capability +discovery. Publish compatibility claims for tested engine/provider combinations, not just a +minimum semver. Qualify automatic behavior for workers selecting compatible support, +including omitted/explicit-only capabilities and per-query declines. ## 12. Required verification @@ -990,7 +1285,7 @@ minimum semver. Enable `auto` by default only for certified providers after perf - Capability absent, empty intersections, unknown selected IDs, version mismatch, size limits, malformed nested structs/schema IPC, duplicate/invalid fields, null versus empty, and invalid status unions. Check typed models/builders as well as schemas across all six worker SDKs. -- `vgi.v2` routing on raw transports and HTTP; new-method schema registration; protocol-name and +- `vgi.v3` routing on raw transports and HTTP; new-method schema registration; protocol-name and surface-version mismatch; the reserved executor still using normal routed `bind`/`init`. - Bulk metadata enabled/disabled, version 0 with/without etag, stale snapshot retry, etag not-modified, transaction-local DDL, and two attachments with coincidentally equal versions. @@ -1008,8 +1303,9 @@ minimum semver. Enable `auto` by default only for certified providers after perf - Same principal with narrowed grant authority; secret ATTACH options; token/SQL redaction; new query APIs cannot bypass LOCATION restrictions or replay authorization from metadata caches. -Run each accepted automatic query against an immutable fixture with pushdown `off` and `auto`; -compare output names/types and result bags or ordered sequences as specified by the query. For +Run each accepted automatic query against paired immutable fixtures: one selects automatic +capability and one omits it, while exposing identical logical tables. Compare output names/types +and result bags or ordered sequences as specified by the query. For nonunique order keys, validate the permitted tie groups rather than demanding an arbitrary identical tie order. Add metamorphic and generated query cases to catch missing overload checks. Use instrumentation to prove accepted queries execute exactly once and declined queries never @@ -1064,10 +1360,11 @@ universal speedup percentage; establish budgets from these measurements before d ## 13. Compatibility, alternatives, and launch gates -The Python SDK and extension currently declare **2.1.0** under **`vgi.v2`**. The query capability -and new RPC target the next coordinated additive surface revision, **2.2.0**, subject to release -coordination if another feature claims that revision first. Do not append them silently to -2.1.0 or rename the protocol to `vgi.v2.2`. +Published main now uses **2.2.0** under **`vgi.v2`**. The prepared Python SDK, other SDKs and extension +use **3.0.0** under **`vgi.v3`**. +The query capability, preparation/release RPCs and parameter metadata join the combined breaking +successor contract. The selected major version and routing/migration policy apply to that package; +do not publish this development surface as a compatible 2.2.0 update. The framework enforces exact major/minor compatibility. Some catalog-item decoders tolerate absent optional fields by name, but the generated request/response validators also compare schema @@ -1078,7 +1375,8 @@ deployment needs matched endpoints/releases or an explicit version-routing strat the same major protocol name alone does not make 2.1 and 2.2 peers interoperable. Wire version, query-profile version, SDK package version, and DuckDB engine version remain -independent. The feature belongs to the VGI 2 family even though its proposed wire release is 2.2. +independent. Query execution joins the combined major successor; it has no separate proposed +2.2 release or independently frozen routing name. Alternatives considered: @@ -1110,7 +1408,7 @@ logical-table authorization equivalence; ordered streaming under parallel client settings; process-safe execution ownership; transaction integration; no accidental result cache use; and coordinated wire-version/codegen validation. -Deferred design work: parameter binding; additional SQL/function/type profiles; remote writes; +Deferred design work: broader backend parameter-type mappings; additional SQL/function/type profiles; remote writes; time travel; nested catalog views; cost-based placement; arbitrary partial subplans; query-result cache validators tied to data snapshots; split result streams with explicit global-order and partition semantics; and durable resumable execution with acknowledged batch IDs. None should diff --git a/docs/protocol/vgi-protocol-proposed-changes.md b/docs/protocol/vgi-protocol-proposed-changes.md index ec57fc7d..3b55f5f7 100644 --- a/docs/protocol/vgi-protocol-proposed-changes.md +++ b/docs/protocol/vgi-protocol-proposed-changes.md @@ -1,5 +1,10 @@ # Proposed VGI protocol changes for DuckDB 2.0 +**Current scope:** the [October 23 combined review](shared-catalog-values.md#combined-protocol-review--october-23-2026) +and [manifest](duckdb-2.0-manifest.json) supersede this historical survey's release +sequence, version candidates and unresolved inclusion lists. Keep this report +as source rationale; it is not the current implementation backlog. + **Status:** design/audit report; the argument-monotonicity subsection records an accepted VGI 2.0 protocol addition **Audit date:** 2026-09-09 **Filter-v2 decision:** 2026-09-09 — mandatory in VGI 2.0; no v1 compatibility path @@ -391,9 +396,18 @@ Leave the callback unset for the initial port. If implemented later, reuse a saf ### Repeatability -`TableFunction::is_repeatable` tells the optimizer whether executions with identical bind data are stable within one query. If the callback is absent, DuckDB treats the function conservatively as not repeatable. That is correct. +The selected [bound scan contract](scan-repeatability.md) appends +`BindResponse.repeatability`, with `UNKNOWN` and `STABLE_WITHIN_QUERY` wire +values. A stable claim belongs to one complete bind state within one statement; +it is independent of scalar-function stability. The native 1.5 adapter carries +the claim and guards snapshot lifetime but has no repeatability callback. -A future `repeatability` metadata value could improve plans, but it must be defined separately from scalar-function stability. A remote table may change between calls even when its schema and arguments do not. Suggested values are `unknown` and `stable_within_query`; do not infer repeatability merely from a read-only-looking function. +DuckDB 2.0's `TableFunction::is_repeatable` supplies the optimizer hook. Its +table-backed scan shortcut classifies a catalog table before consulting that +hook, so the later native port must also address that shortcut to keep unknown +VGI scans conservative. The bounded source implementation and mutable-provider tests passed on the +pinned 1.5 build; production providers and the native 2.0 optimizer port remain +separate qualification work. ### Partition selection @@ -543,6 +557,13 @@ The `ScanTriggers` contract also implies that returned catalog entries remain al ### 6.5 Proposed VGI trigger protocol +**Historical proposal, superseded for the combined successor release:** use +[the shared trigger/statement contract](shared-catalog-values.md#trigger-definitions-and-statement-ownership). +It replaces the engine-to-definition map and broad `supports_triggers` flag with +profiled SQL/context, exclusive execution ownership, current write requirements +and an authoritative statement lifecycle. The names and delivery phases below +are historical source material, not an independent release plan or current RPCs. + The remote VGI catalog should be authoritative for persistent trigger definitions. Keeping definitions only in a local extension cache would lose them on detach and would give different clients inconsistent catalogs. Suggested identity and definition: diff --git a/docs/protocol/vgi-secrets.md b/docs/protocol/vgi-secrets.md index 7525baf7..a517c47f 100644 --- a/docs/protocol/vgi-secrets.md +++ b/docs/protocol/vgi-secrets.md @@ -1,7 +1,7 @@ # VGI Secrets **Status:** Normative. It records the contract the DuckDB extension and the SDKs already implement. -**Protocol:** `vgi.v2`. No wire change. +**Protocol:** `vgi.v3`. No wire change. **Reference implementations:** client, `BuildSecretsBatch` and `ExtractVgiSecrets` in the DuckDB extension; worker, `SecretsAccessor` and `ResolvedSecrets` in vgi-python `vgi/table_function.py`. diff --git a/docs/protocol/view-metadata.md b/docs/protocol/view-metadata.md new file mode 100644 index 00000000..6a73e57c --- /dev/null +++ b/docs/protocol/view-metadata.md @@ -0,0 +1,57 @@ +# View creation metadata and column comments + +This is part of the unpublished coordinated successor protocol. Native builds +remain on DuckDB 1.5; these contracts do not imply native DuckDB 2 qualification. + +`catalog_view_create` now accepts the following metadata together with its +definition, aliases and security mode: + +| Field | Wire type | Meaning | +| --- | --- | --- | +| `comment` | nullable UTF-8 | NULL means no comment; an empty string is a present comment. | +| `tags` | nullable map of UTF-8 to UTF-8 | Omission/NULL creates an empty map. Keys and values remain literal strings. | +| `column_comments` | nullable map of UTF-8 to UTF-8 | Keys are raw output identifiers, including literal dots and quotes. Omission/NULL creates an empty map. Values can be empty strings. | + +Creation and replacement store the complete supplied metadata. REPLACE resets +omitted metadata; IGNORE preserves the existing object. Invalid output keys +(empty or containing NUL) fail before mutation, including IGNORE and REPLACE. +Providers that cannot preserve requested metadata must reject before effects. +The older Python transactor fixture does this explicitly. + +`catalog_view_column_comment_set` takes the attachment, schema path, view name, +`column_path`, nullable `comment`, `ignore_not_found` and optional transaction +handle. `column_path` contains exactly one raw output identifier. A dot in that +component is not a nested path. NULL removes the map entry; an empty string +stores a present empty comment. Missing views obey `ignore_not_found`; invalid +paths still fail. Other view metadata must survive the mutation. + +The Python worker applies its existing barrier capability and transaction checks +before this operation. Metadata-only providers do not bind arbitrary SQL to +check output existence; the native DuckDB adapter binds and validates the target +before sending the RPC. Provider query binding, authorization and concurrent +mutation guarantees remain separate obligations. + +All seven bundled mutable SDK catalogs preserve creation metadata and support +view and view-column comment changes. Their public RPC test exercises creation, +get/list discovery, IGNORE, REPLACE, empty/NULL comments, malformed paths, missing +views and deletion. The durable Python reference additionally tests sealed HTTP +attachments, reconnect, commit and rollback for regular and barrier views. + +Native 1.5 now preserves NULL versus empty view comments and forwards all +`CreateViewInfo` metadata. Its COMMENT routing distinguishes views from tables +and invalidates the affected view cache. A VGI view entry applies explicit +aliases to bound metadata even before the first SELECT, working around 1.5's +initial binding retaining SELECT-derived names. The SQL tests also copy a local +view with comments through `COPY FROM DATABASE ... (SCHEMA)` to exercise creation +metadata without requiring new grammar. + +The Go wire `ViewInfo.Comment` is now `*string` to distinguish NULL from empty. +The older static `CatalogView.Comment` descriptor keeps its historical string +zero-value behavior. Rust adds `ViewCreateOptions` and a public column-comment +client method. TypeScript decodes opaque Arrow map parameters before storage. + +Remaining view work includes full SDK capability negotiation, owner-relative +query resolution, dependencies, fixed output contracts, concurrent replacement, +and native DuckDB 2 barrier execution. The final request shape and major protocol +version/routing decision remain part of the combined contract review. No +intermediate public release is being made. diff --git a/docs/protocol/view-security.md b/docs/protocol/view-security.md new file mode 100644 index 00000000..53a3167c --- /dev/null +++ b/docs/protocol/view-security.md @@ -0,0 +1,84 @@ +# View security in the coordinated successor protocol + +Status: implemented preparation work on the DuckDB 1.5 branch; proposed for the +combined successor release. This document does not qualify DuckDB 2 execution +or close the [release gates](duckdb-2.0-manifest.json). + +## Representation + +`ViewInfo.security` is a required, non-null Arrow `utf8` field after `aliases`. +It has two case-sensitive values: + +| Value | Meaning | +| --- | --- | +| `regular` | An ordinary view. | +| `barrier` | A view whose engine must enforce an optimization barrier. This maps to DuckDB 2 `ViewSecurityType::SECURE_VIEW`. | + +This is not a SQL SECURITY DEFINER/INVOKER or privilege-granting mode. The query +remains a SELECT-only `definition`, accompanied by ordered raw `aliases` and +the existing comment/tag/column-comment metadata. Sending a complete CREATE +VIEW statement in `definition` is not an alternative encoding. + +Missing, NULL, unknown, or wrongly typed read metadata must not be interpreted +as `regular`. Public constructors may default new ordinary views to `regular`, +but serialized metadata must carry the value explicitly. Readers that cannot +enforce a barrier when binding/materializing the query must reject that view +before executing it. This applies regardless of a peer's claimed capabilities. +Metadata-only consumers may retain a barrier record without executing it. + +`catalog_view_create` carries a `security` string after `aliases`. Its ordinary +creation default is `regular`. A barrier request is explicit. Unknown/NULL modes +are rejected; neither IGNORE nor REPLACE allows an unsupported request to lose +the flag silently. A successful provider stores the mode together with the +definition and aliases, and must retain it through metadata changes, discovery, +revalidation, persistence, and transaction completion. + +## Capability agreements + +| Profile | Category | Promise | +| --- | --- | --- | +| `vgi.view.barrier.read.v1` | `read` | Preserve barrier metadata; enforce the barrier if materializing/executing, otherwise reject execution. A provider can supply the explicit mode. | +| `vgi.view.barrier.create.v1` | `ddl` | Accept and persist barrier creation/replacement without semantic loss. Requires the read profile in the same agreement. | + +A client offer is not authorization. Providers still apply their access and +mutation rules. Selection must intersect the offer with current provider and +worker support. A transaction involving barrier DDL additionally requires +`vgi.transaction.ddl.v1`. + +The Python worker checks selected/current profiles on get, schema listing and +full snapshots. It rejects unsupported barrier reads rather than projecting +away the flag. Its capability-aware snapshot hash includes security metadata; +a stale ETag or a token from an incapable attachment cannot bypass the check. +Withdrawing provider support also invalidates permission to use an old sealed +agreement. Checks precede provider CREATE effects, including IGNORE/REPLACE. + +Existing barrier views are checked before replacement, rename, comment changes +and drop. Replacing a barrier with `regular` requires the barrier creation +agreement as well; this prevents an unaware caller's default mode from erasing +the flag. Transactional metadata mutations require the DDL transaction profile. +These worker preflights are not a concurrency primitive. Atomic replacement, +authorization, dependency handling and concurrent mutation/commit semantics +remain provider obligations and broader qualification work. + +## Implemented coverage and limits + +| Layer | Implemented | Still required | +| --- | --- | --- | +| All seven SDKs | Explicit mode in metadata records, descriptors and codecs; CREATE wire parameter; shared regular/barrier IPC vectors. | Full negotiated barrier operation support in every SDK. | +| Python worker | Attachment-scoped read/create profiles, stale-agreement rejection, snapshot/revalidation checks and mutation preflights. | Production provider and concurrent lifecycle qualification. | +| SQLite reference | Public RPC persistence, reconnect, rollback/commit, replacement, rename and comment preservation. | Query binding/execution; this reference only stores metadata. | +| Bundled ordinary mutable providers | Reject barrier/unknown CREATE modes before effects, including IGNORE/REPLACE. | A provider implementation that advertises and fulfills barrier creation. | +| Native DuckDB 1.5 | Always emits ordinary CREATE mode; rejects barrier/missing/NULL/unknown/wrong-type metadata before binding through get, listing and bulk-snapshot paths. | It cannot execute barrier views and does not offer the barrier read profile. | +| Native DuckDB 2 | Source mapping identified. | Set `CreateViewInfo.security_type`, prove optimizer barrier behavior, rebind/cache refresh and dependent-query behavior before offering support. | + +The Python protocol tests exercise the real public HTTP client and durable +reference. SDK tests exercise their actual codecs and unsupported mutation +paths. Native malformed-wire fixtures bypass Python's own checks and assert +which discovery route was used. Passing these tests does not establish native +DuckDB 2 barrier enforcement. + +Creation comments/tags/column comments and view-column comment mutation are +covered by the subsequent [view metadata increment](view-metadata.md). +Owner-relative query resolution, dependencies, fixed output contracts and the +remaining catalog operations stay in the same coordinated release plan. No +intermediate public protocol version is being published. diff --git a/examples/cache_worker.py b/examples/cache_worker.py index 67d6d1e7..e5c2038b 100644 --- a/examples/cache_worker.py +++ b/examples/cache_worker.py @@ -75,7 +75,7 @@ class Meta: description = "Exchange rates from a slow upstream, cacheable for 5 minutes" @classmethod - def process(cls, params: ProcessParams[RatesArgs, None], state: None, out: OutputCollector) -> None: + def process(cls, params: ProcessParams[RatesArgs], state: None, out: OutputCollector) -> None: """Answer the scan — or, if the client's copy is still current, say so.""" vgi_out = cast(VgiOutputCollector, out) diff --git a/packages/vgi-fixtures/pyproject.toml b/packages/vgi-fixtures/pyproject.toml index caf0b1ae..37fbd2d7 100644 --- a/packages/vgi-fixtures/pyproject.toml +++ b/packages/vgi-fixtures/pyproject.toml @@ -8,7 +8,7 @@ # and the conformance suite import it by its normal `vgi._test_fixtures` path. [project] name = "vgi-fixtures" -version = "0.47.0" +version = "0.48.0" description = "Example/test VGI worker modules and entry points (not for production use)" readme = "README.md" # Local copy of the repo-root LICENSE (kept in sync): the build can't reach diff --git a/pyproject.toml b/pyproject.toml index 9fb7b450..0571396e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "vgi-python" -version = "0.47.0" +version = "0.48.0" description = "Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow" readme = "README.md" keywords = [ @@ -40,7 +40,7 @@ dependencies = [ "pyarrow", "typer>=0.9", "platformdirs", - "vgi-rpc>=0.52.0", + "vgi-rpc>=0.52.3", "httpx2>=2.12.0", ] diff --git a/scripts/check_duckdb2_manifest.py b/scripts/check_duckdb2_manifest.py new file mode 100644 index 00000000..ac23a52f --- /dev/null +++ b/scripts/check_duckdb2_manifest.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Check the combined contract inventory against the native field audit. + +This is a coverage check, not proof of implementation or phase completion. +``--require-decisions`` additionally refuses unresolved scope/field reviews. +The audit checkout is required: an unavailable source cannot count as coverage. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +from typing import Any + +from vgi_rpc.rpc._types import rpc_methods # type: ignore[attr-defined] + +from vgi.protocol import VgiProtocol + +_ROOT = Path(__file__).resolve().parents[1] +_MANIFEST = _ROOT / "docs/protocol/duckdb-2.0-manifest.json" +_DISPOSITIONS = {"include", "existing_contract", "existing_contract_plus_engine_port", "engine_only", "defer"} + + +def audit_rows(source: str) -> list[dict[str, Any]]: + """Extract every field/hook row, excluding explanatory and result tables.""" + lines = source.splitlines() + active = False + section = "" + rows = [] + for offset, line in enumerate(lines): + if line.startswith("#"): + section = line.lstrip("# ").strip() + if section == "Shared creation fields and object identity": + active = True + elif section == "Runtime evidence and limits": + break + if not active or not line.startswith("|") or line.startswith("| ---"): + continue + if offset + 1 < len(lines) and lines[offset + 1].startswith("| ---"): + continue + cells = [cell.strip() for cell in line.strip("|").split("|")] + rows.append({"source_line": offset + 1, "section": section, "field": cells[0], "audit_evidence": cells[1:]}) + if not rows: + raise ValueError("No field rows found in the native audit") + return rows + + +def plan_scope(source: str) -> dict[str, list[str]]: + """Read every target and additional candidate directly from the agreed plan.""" + sections = {"Target contract package", "Additional candidates to resolve before freeze"} + section = "" + areas = {} + lines = source.splitlines() + for index, line in enumerate(lines): + if line.startswith("#"): + section = line.lstrip("# ").strip() + if section not in sections or not line.startswith("|") or line.startswith("| ---"): + continue + if index + 1 < len(lines) and lines[index + 1].startswith("| ---"): + continue + title, *scope = [cell.strip() for cell in line.strip("|").split("|")] + if title in areas: + raise ValueError(f"Duplicate plan area: {title}") + areas[title] = scope + if not areas: + raise ValueError("No contract areas found in the native plan") + return areas + + +def check_inventory(manifest: dict[str, Any], native_root: Path) -> list[str]: + """Return inventory errors; proposed records do not imply implemented RPCs.""" + errors = [] + if manifest.get("format_version") != 1: + errors.append("unsupported manifest format") + source_path = native_root / manifest["sources"]["audit_path"] + if not source_path.is_file(): + return [f"required audit source is missing: {source_path}"] + source = source_path.read_bytes() + if hashlib.sha256(source).hexdigest() != manifest["sources"]["audit_sha256"]: + errors.append("native audit digest changed; reconcile the manifest against the new evidence") + rows = audit_rows(source.decode()) + fields = manifest["audit_fields"] + actual = [{key: entry.get(key) for key in row} for entry, row in zip(fields, rows, strict=False)] + if len(fields) != len(rows) or actual != rows: + errors.append("audit field inventory differs: every row and its source evidence must be retained in order") + if len({entry["id"] for entry in fields}) != len(fields): + errors.append("duplicate audit entry IDs") + areas = manifest["areas"] + plan_path = native_root / manifest["sources"]["plan_path"] + if not plan_path.is_file(): + errors.append(f"required plan source is missing: {plan_path}") + else: + expected_scope = plan_scope(plan_path.read_text()) + actual_scope = {area["title"]: area["scope"] for area in areas.values()} + if len(actual_scope) != len(areas): + errors.append("duplicate contract area titles") + for title, scope in expected_scope.items(): + if actual_scope.get(title) != scope: + errors.append(f"{title}: plan scope is missing or differs; reconcile the agreed requirement") + methods = set(rpc_methods(VgiProtocol)) + for name, area in areas.items(): + if not area.get("scope") or not area.get("records"): + errors.append(f"{name}: missing scope or target record inventory") + missing = set(area["existing_methods"]) - methods + if missing: + errors.append(f"{name}: listed existing RPCs do not exist: {sorted(missing)}") + for entry in fields: + if entry["area"] not in areas: + errors.append(f"{entry['id']}: unknown contract area {entry['area']}") + if entry["disposition"] != "engine_only" and not entry.get("target"): + errors.append(f"{entry['id']}: no field/record target") + for proposal in manifest["proposals"]: + missing = set(proposal["areas"]) - areas.keys() + if missing: + errors.append(f"{proposal['source']}: unknown areas {sorted(missing)}") + return errors + + +def pending_decisions(manifest: dict[str, Any]) -> list[str]: + """Identify scope and field reviews still needed for the P0 exit gate.""" + pending = [] + for name, area in manifest["areas"].items(): + if area["disposition"] not in _DISPOSITIONS: + pending.append(f"{name}: inclusion decision is unresolved") + if area["disposition"] == "defer" and not (area.get("reason") and area.get("later_release_cost")): + pending.append(f"{name}: deferral lacks its reason or later protocol-release cost") + if not area.get("capability_contract") or area["capability_contract"] == "pending_P1": + pending.append(f"{name}: capability mapping is unresolved") + for entry in manifest["audit_fields"]: + if entry["disposition"] not in _DISPOSITIONS or entry["status"] == "pending_contract_review": + pending.append(f"{entry['id']}: field mapping still requires review") + return pending + + +def main() -> int: + """Validate coverage and optionally require all scope decisions to be settled.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", type=Path, default=_MANIFEST) + parser.add_argument("--native-root", type=Path, default=_ROOT.parent / "vgi") + parser.add_argument("--require-decisions", action="store_true") + args = parser.parse_args() + manifest = json.loads(args.manifest.read_text()) + errors = check_inventory(manifest, args.native_root) + if errors: + for error in errors: + print(f"FAIL: {error}") + return 1 + pending = pending_decisions(manifest) + print(f"Inventory covers {len(manifest['audit_fields'])} audit rows and {len(manifest['areas'])} contract areas.") + print(f"{len(pending)} scope/capability/field reviews remain; inventory coverage is not phase completion.") + if args.require_decisions and pending: + for item in pending: + print(f"PENDING: {item}") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/hosted_protocols_conformance.sh b/scripts/hosted_protocols_conformance.sh index ffc4430c..65e686fd 100755 --- a/scripts/hosted_protocols_conformance.sh +++ b/scripts/hosted_protocols_conformance.sh @@ -8,7 +8,7 @@ # unix vgi-fixture-worker --unix (the DuckDB launcher path) # http vgi-fixture-http --identity (+ the Identity.v2 groups) # -# Every worker must host, in order, vgi.v2 then conformance.Secondary.v1. +# Every worker must host, in order, vgi.v3 then conformance.Secondary.v1. # # Usage: scripts/hosted_protocols_conformance.sh [stdio|unix|http ...] # Default: all three. Runs from the project env (`uv run --no-sync`), which @@ -17,7 +17,7 @@ set -euo pipefail cd "$(dirname "$0")/.." -EXPECT="vgi.v2,conformance.Secondary.v1" +EXPECT="vgi.v3,conformance.Secondary.v1" RUN=(uv run --no-sync) BIN="$(dirname "$("${RUN[@]}" python -c 'import sys; print(sys.executable)')")" TRANSPORTS=("$@") diff --git a/scripts/regen_generated.py b/scripts/regen_generated.py index 0b8ce2bb..2afd98c4 100644 --- a/scripts/regen_generated.py +++ b/scripts/regen_generated.py @@ -5,6 +5,7 @@ uv run --project ~/Development/vgi-python python scripts/regen_generated.py uv run --project ~/Development/vgi-python python scripts/regen_generated.py --check + uv run --project ~/Development/vgi-python python scripts/regen_generated.py --check --require-all Why this exists rather than a shell one-liner per artifact: the obvious form, ``python -m vgi.codegen.X > path/to/generated.file``, truncates the destination @@ -21,6 +22,7 @@ so regenerating one SDK's worktree writes nothing into any other checkout: uv run python scripts/regen_generated.py --only vgi-go --root vgi-go=~/Development/vgi-go-feature +Release checks use ``--require-all`` to reject missing selected repositories before writing. """ from __future__ import annotations @@ -36,6 +38,7 @@ #: ``vgi-java`` lives at ``~/vgi-java`` for some checkouts and beside the other #: SDKs for others; both are probed. _TARGETS: list[tuple[str, str, str]] = [ + ("vgi.codegen.protocol_version", "vgi/protocol_version.txt", "vgi-python"), ("vgi.codegen.cpp_schemas", "src/generated/vgi_protocol_schemas.hpp", "vgi"), ("vgi.codegen.cpp_request_builders", "src/generated/vgi_request_builders.hpp", "vgi"), ("vgi.codegen.cpp_constants", "src/generated/vgi_protocol_constants.hpp", "vgi"), @@ -79,6 +82,18 @@ ), ] +# One codec corpus, checked into each SDK so its conformance tests are standalone. +_TARGETS += [ + ("vgi.codegen.catalog_contract_fixtures", "conformance/catalog-contracts.json", repo) + for repo in ("vgi-python", "vgi-go", "vgi-rust", "vgi-typescript", "vgi-java", "vgi-csharp", "vgi-c++") +] + +# Reporting contracts have an index plus one reference page per source module. +_TARGETS += [ + (f"vgi.codegen.reporting_docs:{record}", relative, "vgi-python") + for record, relative in importlib.import_module("vgi.codegen.reporting_docs").targets() +] + # vgi-java's generated wire records: one file per record (Java allows one public # top-level type per file), so one target each. ``module:record`` tells # `_render` to call ``emit(out, record=...)``. Derived from the generator's own @@ -89,20 +104,32 @@ ] -# The vgi.v2 registries: one generator, one backend per SDK (see +# The vgi.v3 registries: one generator, one backend per SDK (see # vgi.codegen._registry). Each declares its own target, checkout and override. _REGISTRY_BACKENDS = importlib.import_module("vgi.codegen._registry_backend").registry_backends() _TARGETS += [(b.module, b.target, b.repo) for b in _REGISTRY_BACKENDS] +# The C++ worker's public headers use a different namespace from the native +# extension. They belong in the same required inventory as its method registry. +_TARGETS += [ + (f"vgi.codegen.cpp_sdk:{record}", f"include/vgi/generated/vgi_protocol_{suffix}.hpp", "vgi-c++") + for record, suffix in ( + ("schemas", "schemas"), + ("constants", "constants"), + ("version", "version"), + ("names", "names"), + ) +] + #: Environment overrides for a checkout's location (``VGI_TYPESCRIPT_ROOT=...``). -_ROOT_ENV: dict[str, str] = {b.repo: b.root_env for b in _REGISTRY_BACKENDS} -# The DuckDB extension checkout (e.g. a worktree of it). -_ROOT_ENV.setdefault("vgi", "VGI_EXTENSION_ROOT") +_ROOT_ENV: dict[str, str] = {"vgi": "VGI_NATIVE_ROOT", **{b.repo: b.root_env for b in _REGISTRY_BACKENDS}} def _repo_root(name: str, roots: dict[str, str]) -> Path | None: """Locate a sibling checkout, or ``None`` when it is not present.""" override = roots.get(name) or os.environ.get(_ROOT_ENV.get(name, "")) + if not override and name == "vgi": + override = os.environ.get("VGI_EXTENSION_ROOT") if override: return Path(override) if Path(override).is_dir() else None siblings = Path(__file__).resolve().parents[2] @@ -112,7 +139,7 @@ def _repo_root(name: str, roots: dict[str, str]) -> Path | None: return None -def _render(module_name: str) -> str: +def _render(module_name: str, native_cpp_backend: str | None = None) -> str: """Render one artifact into memory. Raises if the generator fails. ``module:record`` renders one file of a generator that emits several @@ -121,10 +148,10 @@ def _render(module_name: str) -> str: module_name, _, record = module_name.partition(":") module = importlib.import_module(module_name) buf = io.StringIO() - if record: - module.emit(buf, record=record) - else: - module.emit(buf) + kwargs = {"record": record} if record else {} + if native_cpp_backend is not None: + kwargs["backend"] = native_cpp_backend + module.emit(buf, **kwargs) text = buf.getvalue() if not text.strip(): raise RuntimeError(f"{module_name}.emit() produced no output") @@ -153,6 +180,17 @@ def main() -> int: metavar="REPO=PATH", help="the checkout to use for REPO (repeatable), e.g. --root vgi-go=../vgi-go-feature", ) + parser.add_argument( + "--require-all", + action="store_true", + help="fail before writing if any selected repository is missing", + ) + parser.add_argument( + "--native-cpp-backend", + choices=("arrow", "nanoarrow"), + default="arrow", + help="native extension runtime only; standalone C++ SDK stays Arrow C++", + ) args = parser.parse_args() known = {repo for _, _, repo in _TARGETS} unknown = set(args.only) - known @@ -165,18 +203,37 @@ def main() -> int: parser.error(f"--root expects REPO=PATH with REPO one of {sorted(known)}, got {spec!r}") roots[repo] = str(Path(path).expanduser()) + targets = [(module, relative, repo) for module, relative, repo in _TARGETS if not args.only or repo in args.only] + roots = {repo: _repo_root(repo, roots) for _, _, repo in targets} + if args.require_all: + missing = sorted(repo for repo, root in roots.items() if root is None) + if missing: + for repo in missing: + print(f"FAIL required repository {repo} is not checked out", file=sys.stderr) + return 2 + stale, wrote, skipped, failed = [], [], [], [] - for module_name, relative, repo in _TARGETS: - if args.only and repo not in args.only: - continue - root = _repo_root(repo, roots) + for module_name, relative, repo in targets: + root = roots[repo] if root is None: skipped.append(f"{repo} (not checked out)") continue dest = root / relative try: - text = _render(module_name) + native_runtime_modules = { + "vgi.codegen.cpp_schemas", + "vgi.codegen.cpp_request_builders", + "vgi.codegen.cpp_records", + "vgi.codegen.cpp_secret_schemas", + "vgi.codegen.cpp_secret_request_builders", + } + backend = ( + args.native_cpp_backend + if repo == "vgi" and module_name.partition(":")[0] in native_runtime_modules + else None + ) + text = _render(module_name, backend) if backend is not None else _render(module_name) except Exception as exc: # noqa: BLE001 - report every failure, don't stop failed.append(f"{module_name}: {exc}") continue diff --git a/tests/_hosted_protocols_worker.py b/tests/_hosted_protocols_worker.py index 16a8263f..b42ab374 100644 --- a/tests/_hosted_protocols_worker.py +++ b/tests/_hosted_protocols_worker.py @@ -55,7 +55,7 @@ def count(self, text: str) -> int: class HostingWorker(Worker): - """Hosts `EchoProtocol` then `CounterProtocol` beside ``vgi.v2``.""" + """Hosts `EchoProtocol` then `CounterProtocol` beside ``vgi.v3``.""" functions = [] diff --git a/tests/catalog/column_native_reference.py b/tests/catalog/column_native_reference.py new file mode 100644 index 00000000..17bfc356 --- /dev/null +++ b/tests/catalog/column_native_reference.py @@ -0,0 +1,166 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D106, D107 + +"""SQL integration bridge to the durable column reference, using baseline INSERT. + +This small fixture reads and appends rows in the same SQLite snapshots as DDL. +It is not a production transactor: UPDATE, DELETE, RETURNING, replay, and +distributed transactions are not implemented or advertised. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass, replace +from typing import Any + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import OutputCollector + +from vgi.catalog import Catalog, ReadOnlyCatalogInterface, ScanFunctionResult, Schema, SchemaObjectType +from vgi.catalog.sql import ColumnExpressions, with_column_expressions +from vgi.invocation import BindResponse, GlobalInitResponse +from vgi.table_function import BindParams, InitParams, ProcessParams, TableFunctionGenerator +from vgi.table_in_out_function import TableInOutGenerator +from vgi.worker import Worker + +from .column_paths_reference import ColumnPathsCatalog +from .typing_helpers import present + +COUNT = pa.schema([pa.field("count", pa.int64(), nullable=False)]) + + +def _args(params: Any) -> dict[str, Any]: + call = params.bind_call if isinstance(params, BindParams) else params.init_call.bind_call + target = json.loads(present(call.arguments.positional[0]).as_py()) + return dict( + attach_opaque_data=params.attach_opaque_data, + transaction_opaque_data=params.transaction_opaque_data, + **target, + ) + + +@dataclass +class ScanState(ArrowSerializableDataclass): + """Track whether the transaction snapshot has been emitted.""" + + emitted: bool = False + + +class ReferenceScan(TableFunctionGenerator[None, ScanState]): + """Expose physical reference rows through normal VGI TABLE execution.""" + + class Meta: + name = "column_reference_scan" + projection_pushdown = False + filter_pushdown = False + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + info = NativeColumnsCatalog().table_get(**_args(params)) + if info is None: + raise ValueError("table does not exist") + + def data_field(field: pa.Field[Any]) -> pa.Field[Any]: + # Column definitions live on TableInfo; the scan's data schema must + # not ask generic function consumers to interpret catalog defaults. + field = with_column_expressions(field, ColumnExpressions()) + if pa.types.is_struct(field.type): + field = field.with_type(pa.struct([data_field(child) for child in field.type])) + return field + + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) + return BindResponse( + output_schema=pa.schema( + [data_field(field) for field in schema], metadata={k: v for k, v in (schema.metadata or {}).items()} + ) + ) + + @classmethod + def on_init(cls, params: InitParams[None]) -> GlobalInitResponse: + return GlobalInitResponse(max_workers=1) + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> ScanState: + return ScanState() + + @classmethod + def process(cls, params: ProcessParams[None], state: ScanState, out: OutputCollector) -> None: + if state.emitted: + out.finish() + else: + partitions = NativeColumnsCatalog().physical_rows(**_args(params)) + rows = [row for partition in partitions for row in partition["rows"]] + out.emit(pa.RecordBatch.from_pylist(rows, schema=params.output_schema)) + state.emitted = True + + +class ReferenceInsert(TableInOutGenerator[None, None]): + """Append count-mode rows within the catalog's existing transaction snapshot.""" + + class Meta: + name = "column_reference_insert" + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + from vgi._test_fixtures.writable.table import _parse_write_options + + options = _parse_write_options(params.bind_call) + if options.get("result_mode", "count") != "count" or options.get("on_conflict", "throw") != "throw": + raise NotImplementedError("reference supports count INSERT without conflict handling") + return BindResponse(output_schema=COUNT) + + @classmethod + def process(cls, params: ProcessParams[None], state: None, batch: pa.RecordBatch, out: OutputCollector) -> None: + NativeColumnsCatalog().seed_rows(**_args(params), rows=batch.to_pylist()) + out.emit(pa.RecordBatch.from_pylist([{"count": batch.num_rows}], schema=COUNT)) + + +CATALOG = Catalog( + name="columns", + default_schema="main", + schemas=[Schema(path=["main"], functions=[ReferenceScan, ReferenceInsert])], +) + + +class NativeColumnsCatalog(ColumnPathsCatalog): + """Add baseline scans/INSERT to the same durable DDL reference.""" + + catalog = CATALOG + database_path = os.environ["VGI_COLUMN_REFERENCE_DB"] + + def schema_contents(self, **kwargs: Any) -> Any: + if kwargs["type"] == SchemaObjectType.TABLE: + return [replace(info, write_result_modes={"insert": "count"}) for info in super().schema_contents(**kwargs)] + return ReadOnlyCatalogInterface.schema_contents(self, **kwargs) + + def table_get(self, **kwargs: Any) -> Any: + info = super().table_get(**kwargs) + return replace(info, write_result_modes={"insert": "count"}) if info is not None else None + + def _function(self, kind: str, **kwargs: Any) -> ScanFunctionResult: + target = json.dumps({"schema_path": kwargs["schema_path"], "name": kwargs["name"]}) + return ScanFunctionResult( + function_name=f"column_reference_{kind}", + positional_arguments=[pa.scalar(target)], + named_arguments={}, + ) + + def table_scan_function_get(self, **kwargs: Any) -> ScanFunctionResult: + return self._function("scan", **kwargs) + + def table_insert_function_get(self, **kwargs: Any) -> ScanFunctionResult: + return self._function("insert", **kwargs) + + +class NativeColumnsWorker(Worker): + """Host the fixture using normal protocol, binding and streaming paths.""" + + catalog_interface = NativeColumnsCatalog + catalog = CATALOG + + +if __name__ == "__main__": + NativeColumnsWorker.main() diff --git a/tests/catalog/column_paths_reference.py b/tests/catalog/column_paths_reference.py new file mode 100644 index 00000000..f14e70bf --- /dev/null +++ b/tests/catalog/column_paths_reference.py @@ -0,0 +1,482 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D107 + +"""Durable column-edit reference with real nested data changes on the 1.5 engine. + +This fixture intentionally supports primitive JSON values and structs, refuses +layout dependencies and generated columns, and advertises no public DML path. +Its transaction snapshots exercise DDL rollback without claiming distributed or +mixed-write atomicity. General expression profiles remain a separate contract. +""" + +from __future__ import annotations + +import base64 +import copy +import json +import sqlite3 +import uuid +from contextlib import closing +from dataclasses import replace +from typing import Any + +import duckdb +import pyarrow as pa +import pyarrow.compute as pc + +from vgi.catalog import TableInfo +from vgi.catalog.capabilities import ( + COLUMN_NESTED_ADD, + COLUMN_NESTED_COMMENT, + COLUMN_NESTED_DEFAULT, + COLUMN_NESTED_DROP, + COLUMN_NESTED_NULLABILITY, + COLUMN_NESTED_RENAME, + COLUMN_NESTED_TYPE, + CONSTRAINT_CREATE, + CONSTRAINT_ENFORCE, + CONSTRAINT_MUTATE, + CONSTRAINT_RECORDS_READ, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import SerializedSchema +from vgi.catalog.column_metadata import column_metadata +from vgi.catalog.constraints import ( + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, + ConstraintValidation, + constraint_definitions, + validate_constraint_schema, +) +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.layout import SqlExpression +from vgi.catalog.sql import ColumnExpressions, column_expressions, with_column_expressions +from vgi.column_path import column_definition, column_path_key, replace_column_field, resolve_column_path +from vgi.worker import Worker + +from .expression_reference import validate_expression as _validate_expression +from .table_layout_reference import TABLE_PROFILES, TableLayoutCatalog, _table_key +from .typing_helpers import present + +COLUMN_PROFILES = CatalogCapabilities( + read=sorted([*TABLE_PROFILES.read, CONSTRAINT_RECORDS_READ]), + ddl=sorted( + [ + *TABLE_PROFILES.ddl, + COLUMN_NESTED_ADD, + COLUMN_NESTED_DROP, + COLUMN_NESTED_RENAME, + COLUMN_NESTED_TYPE, + COLUMN_NESTED_DEFAULT, + COLUMN_NESTED_NULLABILITY, + COLUMN_NESTED_COMMENT, + CONSTRAINT_CREATE, + CONSTRAINT_MUTATE, + ] + ), + write=[CONSTRAINT_ENFORCE], + transaction=TABLE_PROFILES.transaction, +) + + +def _eval( + expression: SqlExpression, + path: list[str], + schema: pa.Schema | None = None, + rows: list[dict[str, Any]] | None = None, +) -> pa.Array[Any]: + expression.validate_owner(path) + with duckdb.connect(config={"enable_external_access": False}) as connection: + connection.execute("SET TimeZone='UTC'") + connection.execute("SET default_collation='binary'") + connection.execute("SET default_order='asc'") + connection.execute("SET default_null_order='nulls_last'") + connection.execute("SET preserve_insertion_order=true") + if schema is not None: + connection.register("input_data", pa.Table.from_pylist(rows or [], schema=schema)) + _validate_expression( + connection, + expression, + path, + role="using" if schema is not None else "default", + ) + query = f"SELECT ({expression.sql}) AS v" + (" FROM input_data" if schema is not None else "") + return connection.execute(query).to_arrow_table().column(0).combine_chunks() + + +def _fields(fields: Any, path: list[str]) -> None: + """Validate fixture support before storing any metadata or rows.""" + names = [field.name for field in fields] + if len(names) != len(set(names)): + raise ValueError("duplicate field names") + for field in fields: + column_metadata(field) + column_path_key([field.name]) + if pa.types.is_struct(field.type): + if not len(field.type): + raise ValueError("empty structs are not supported") + _fields(field.type, path) + elif not any( + ( + pa.types.is_integer(field.type), + pa.types.is_floating(field.type), + pa.types.is_string(field.type), + pa.types.is_boolean(field.type), + ) + ): + raise NotImplementedError("reference supports primitive JSON values and structs") + metadata = field.metadata or {} + expressions = column_expressions(field) + if expressions.generated is not None or metadata.get(b"compression", b"auto") != b"auto": + raise NotImplementedError("generated expressions and nondefault compression are not supported") + if {b"default", b"generated_expression"}.intersection(metadata): + raise ValueError("column reference requires canonical expression metadata") + if any(key.startswith(b"ARROW:extension:") for key in metadata): + raise NotImplementedError("logical extensions need their own semantic qualification") + if expressions.default is not None: + pc.cast(_eval(expressions.default, path), field.type, safe=True) + + +def _validate_values(fields: Any, value: dict[str, Any]) -> None: + if set(value) - {field.name for field in fields}: + raise ValueError("row contains unknown fields") + for field in fields: + item = value.get(field.name) + if item is None: + if not field.nullable: + raise ValueError(f"NOT NULL violation for {field.name}") + elif pa.types.is_struct(field.type): + _validate_values(field.type, item) + + +def _defaulted(fields: Any, row: dict[str, Any], schema_path: list[str]) -> dict[str, Any]: + result = copy.deepcopy(row) + for field in fields: + default = column_expressions(field).default + if field.name not in result and default is not None: + result[field.name] = pc.cast(_eval(default, schema_path), field.type, safe=True)[0].as_py() + if pa.types.is_struct(field.type) and result.get(field.name) is not None: + result[field.name] = _defaulted(field.type, result[field.name], schema_path) + return result + + +def _parent(row: dict[str, Any], path: list[str]) -> dict[str, Any] | None: + value: dict[str, Any] | None = row + for part in path[:-1]: + value = value.get(part) if value is not None else None + return value + + +class ColumnPathsCatalog(TableLayoutCatalog): + """Store each metadata/data edit in the same SQLite transaction snapshot.""" + + catalog_name = "columns" + profiles = COLUMN_PROFILES + + def table_create(self, **kwargs: Any) -> None: + _fields(pa.ipc.read_schema(pa.py_buffer(kwargs["columns"])), kwargs["schema_path"]) + super().table_create(**kwargs) + + def _create_constraints(self, args: dict[str, Any]) -> tuple[list[bytes], bytes | None]: + table = CatalogObjectRef(kind="table", schema_path=args["schema_path"], name=args["name"]) + definitions = constraint_definitions(args["constraint_definitions"], table) + if any(definition.kind != ConstraintKind.NOT_NULL for definition in definitions): + raise NotImplementedError("column reference only enforces NOT_NULL constraints") + validate_constraint_schema(pa.ipc.read_schema(pa.py_buffer(args["columns"])), definitions) + return [ + self._not_null_info(table, definition).serialize_to_bytes() for definition in definitions + ], uuid.uuid4().bytes + + @staticmethod + def _not_null_info(table: CatalogObjectRef, definition: ConstraintDefinition) -> ConstraintInfo: + return ConstraintInfo( + ref=CatalogObjectRef( + kind="constraint", schema_path=table.schema_path, owner_table=table.name, name=uuid.uuid4().hex + ), + definition=definition, + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + ) + + def _changed_constraints( + self, info: TableInfo, schema: pa.Schema, operation: str, path: list[str], replacement: pa.Field[Any] | None + ) -> list[bytes]: + existing = {} + for constraint in info.constraint_infos(): + key = constraint.definition.column_paths[0] + if key[: len(path)] == path: + if operation == "drop": + continue + if operation == "rename": + assert replacement is not None + key = [*path[:-1], replacement.name, *key[len(path) :]] + constraint = replace(constraint, definition=replace(constraint.definition, column_paths=[key])) + existing[tuple(key)] = constraint + table = CatalogObjectRef(kind="table", schema_path=info.schema_path, name=info.name) + result = [] + + def collect(fields: Any, prefix: list[str]) -> None: + for field in fields: + key = [*prefix, field.name] + if not field.nullable: + constraint = existing.get(tuple(key)) + if constraint is None: + constraint = self._not_null_info( + table, ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[key]) + ) + result.append(constraint.serialize_to_bytes()) + if pa.types.is_struct(field.type): + collect(field.type, key) + + collect(schema, []) + return result + + def _arrange(self, rows: list[dict[str, Any]], info: TableInfo) -> list[dict[str, Any]]: + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) + # Resolve defaults from the same locked snapshot that stores the rows. + # Reading table metadata before _change would race concurrent DDL. + rows = [_defaulted(schema, row, info.schema_path) for row in rows] + for row in rows: + _validate_values(schema, row) + return super()._arrange(rows, info) + + def _column_change(self, operation: str, args: dict[str, Any]) -> ConstraintSnapshot | None: + path = [] if "constraint_request" in args else list(column_path_key(args["column_path"])) + key = _table_key(args["schema_path"], args["name"]) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, args["attach_opaque_data"], args["transaction_opaque_data"]) + if key not in data: + if args.get("ignore_not_found"): + return None + raise ValueError("table does not exist") + info = self._info(data[key]) + request = args.get("constraint_request") + if request is not None: + if request.expected_revision != info.definition_revision: + raise ValueError("stale table definition revision") + change = request.constraint_changes()[0] + if change.operation == ConstraintOperation.ADD: + path = change.definition.column_paths[0] + if any(item.definition.column_paths == [path] for item in info.constraint_infos()): + raise ValueError("NOT_NULL constraint already exists") + operation = "not_null_set" + else: + constraint_matches = [ + item for item in info.constraint_infos() if item.ref.key() == change.ref.key() + ] + if len(constraint_matches) != 1 or constraint_matches[0].definition.kind != ConstraintKind.NOT_NULL: + raise ValueError("unknown NOT_NULL constraint identity") + path = constraint_matches[0].definition.column_paths[0] + operation = "not_null_drop" + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) + parent_fields = schema if len(path) == 1 else resolve_column_path(schema, path[:-1]).type + if not isinstance(parent_fields, pa.Schema) and not pa.types.is_struct(parent_fields): + raise ValueError("column path has a non-struct parent") + matches = [field for field in parent_fields if field.name == path[-1]] + if len(matches) > 1: + raise ValueError("column path is ambiguous") + existing = matches[0] if matches else None + if operation == "add" and existing is not None: + if args.get("if_column_not_exists"): + return None + raise ValueError("column already exists") + if operation != "add" and existing is None: + if operation == "drop" and args.get("if_column_exists"): + return None + raise ValueError("column does not exist") + if ( + operation in ("drop", "rename", "type") + and info.layout is not None + and (info.layout.partition_by or info.layout.sorted_by) + ): + raise NotImplementedError("layout dependencies must be resolved before changing this column") + record = json.loads(data[key]) + rows = [copy.deepcopy(row) for part in record["partitions"] for row in part["rows"]] + replacement = existing + computed = None + if operation == "add": + replacement = column_definition(args["column_definition"], path) + _fields([replacement], info.schema_path) + default = column_expressions(replacement).default + for row in rows: + parent = _parent(row, path) + if parent is not None: + parent[path[-1]] = ( + pc.cast(_eval(default, info.schema_path), present(replacement).type, safe=True)[0].as_py() + if default is not None + else None + ) + elif operation == "drop": + replacement = None + for row in rows: + parent = _parent(row, path) + if parent is not None: + parent.pop(path[-1], None) + elif operation == "rename": + assert existing is not None + replacement = existing.with_name(args["new_column_name"]) + for row in rows: + parent = _parent(row, path) + if parent is not None: + parent[replacement.name] = parent.pop(path[-1], None) + elif operation == "type": + assert existing is not None + target = column_definition(args["column_definition"], path, type_only=True) + # This reference rejects logical extensions; retain all other + # annotations when replacing the primitive physical type. + metadata = dict(existing.metadata or {}) + metadata.update(target.metadata or {}) + replacement = existing.with_type(target.type) + replacement = ( + replacement.with_metadata({k: v for k, v in metadata.items()}) + if metadata + else replacement.remove_metadata() + ) + _fields([replacement], info.schema_path) + if args["expression"] is not None: + # Bind before changes even if the table has no rows. + computed = pc.cast( + _eval(args["expression"], info.schema_path, schema, rows), target.type, safe=True + ) + else: + values = [ + parent.get(path[-1]) if (parent := _parent(row, path)) is not None else None for row in rows + ] + computed = pc.cast(pa.array(values, type=existing.type), target.type, safe=True) + for index, row in enumerate(rows): + parent = _parent(row, path) + if parent is not None: + parent[path[-1]] = computed[index].as_py() + else: + assert existing is not None + metadata = dict(existing.metadata or {}) + if operation == "default_set": + pc.cast(_eval(args["expression"], info.schema_path), existing.type, safe=True) + metadata = dict( + with_column_expressions(existing, ColumnExpressions(default=args["expression"])).metadata or {} + ) + elif operation == "default_drop": + metadata = dict(with_column_expressions(existing, ColumnExpressions()).metadata or {}) + elif operation == "comment": + if args["comment"] is None: + metadata.pop(b"comment", None) + else: + metadata[b"comment"] = args["comment"].encode() + elif operation in ("not_null_set", "not_null_drop"): + replacement = existing.with_nullable(operation == "not_null_drop") + else: + raise AssertionError(operation) + assert replacement is not None + replacement = ( + replacement.with_metadata({k: v for k, v in metadata.items()}) + if metadata + else replacement.remove_metadata() + ) + updated = replace_column_field(schema, path, replacement, add=operation == "add") + constraints = self._changed_constraints(info, updated, operation, path, replacement) + changed = not updated.equals(schema, check_metadata=True) or constraints != info.constraints + info = replace( + info, + columns=SerializedSchema(updated.serialize().to_pybytes()), + constraints=constraints, + definition_revision=uuid.uuid4().bytes + if changed and operation != "comment" + else info.definition_revision, + ) + # Metadata-only edits must preserve physical layout, especially + # when a future-write layout differs from existing partitions. + for row in rows: + _validate_values(updated, row) + partitions = ( + self._arrange(rows, info) if operation in ("add", "drop", "rename", "type") else record["partitions"] + ) + data[key] = json.dumps( + {"info": base64.b64encode(info.serialize_to_bytes()).decode(), "partitions": partitions} + ) + tx = args["transaction_opaque_data"] + if tx is None: + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version + 1, json.dumps(data))) + else: + db.execute("UPDATE transactions SET version=?, data=? WHERE id=?", (version + 1, json.dumps(data), tx)) + if request is not None: + return ConstraintSnapshot( + table=request.table, + definition_revision=present(info.definition_revision), + constraints=info.constraints, + ) + return None + + def table_constraints_apply(self, request: ConstraintsApplyRequest) -> ConstraintSnapshot: + """Apply the native single NOT_NULL edit in the same locked data snapshot.""" + request.__post_init__() + changes = request.constraint_changes() + if len(changes) != 1: + raise NotImplementedError("column reference supports one NOT_NULL change per request") + change = changes[0] + if change.operation == ConstraintOperation.ADD: + definition = change.definition + if ( + present(definition).kind != ConstraintKind.NOT_NULL + or present(definition).declared_name is not None + or change.enforcement != ConstraintEnforcement.ENFORCED + or change.validation != ConstraintValidation.ALL_ROWS + ): + raise NotImplementedError("column reference supports unnamed enforced ALL_ROWS NOT_NULL only") + elif change.operation != ConstraintOperation.DROP or change.cascade: + raise NotImplementedError("column reference supports NOT_NULL ADD and non-cascading DROP only") + result = self._column_change( + "constraint", + dict( + attach_opaque_data=request.attach_opaque_data, + transaction_opaque_data=request.transaction_opaque_data, + schema_path=request.table.schema_path, + name=request.table.name, + constraint_request=request, + ), + ) + assert result is not None + return result + + def table_column_add(self, **kwargs: Any) -> None: + self._column_change("add", kwargs) + + def table_column_drop(self, **kwargs: Any) -> None: + self._column_change("drop", kwargs) + + def table_column_rename(self, **kwargs: Any) -> None: + self._column_change("rename", kwargs) + + def table_column_type_change(self, **kwargs: Any) -> None: + self._column_change("type", kwargs) + + def table_column_default_set(self, **kwargs: Any) -> None: + self._column_change("default_set", kwargs) + + def table_column_default_drop(self, **kwargs: Any) -> None: + self._column_change("default_drop", kwargs) + + def table_not_null_set(self, **kwargs: Any) -> None: + self._column_change("not_null_set", kwargs) + + def table_not_null_drop(self, **kwargs: Any) -> None: + self._column_change("not_null_drop", kwargs) + + def table_column_comment_set(self, **kwargs: Any) -> None: + self._column_change("comment", kwargs) + + +class ColumnPathsWorker(Worker): + """Expose the durable column reference through the normal protocol worker.""" + + def _get_catalog(self) -> ColumnPathsCatalog: + catalog = super()._get_catalog() + assert isinstance(catalog, ColumnPathsCatalog) + return catalog + + catalog_interface = ColumnPathsCatalog diff --git a/tests/catalog/constraints_protocol_reference.py b/tests/catalog/constraints_protocol_reference.py new file mode 100644 index 00000000..9aaca234 --- /dev/null +++ b/tests/catalog/constraints_protocol_reference.py @@ -0,0 +1,357 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103, D107 + +"""Public catalog adapter for the finite durable constraint authority. + +No scan or write function is advertised. Provider-side row replacements use the +same authority and enforce every constraint; the public surface proves catalog +creation, discovery, mutation and DDL transactions without inventing DML support. +""" + +from __future__ import annotations + +import os +import sqlite3 +import uuid +from collections.abc import Sequence +from contextlib import closing +from dataclasses import replace +from typing import Any, ClassVar, Literal, overload + +import pyarrow as pa + +from vgi.catalog import ( + AttachOpaqueData, + CatalogAttachResult, + FunctionInfo, + IndexInfo, + MacroInfo, + OnConflict, + ReadOnlyCatalogInterface, + SchemaInfo, + SchemaObjectType, + TableInfo, + TransactionOpaqueData, + ViewInfo, +) +from vgi.catalog.capabilities import ( + CONSTRAINT_CREATE, + CONSTRAINT_ENFORCE, + CONSTRAINT_MUTATE, + CONSTRAINT_RECORDS_READ, + ROW_SCALAR_EXPRESSION, + TRANSACTION_DDL, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import CatalogContentsResult, SchemaContentsInfo, SerializedSchema +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintEnforcement, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, + ConstraintValidation, +) +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.schema_path import SchemaPath +from vgi.worker import Worker + +from .constraints_reference import ConstraintCatalog, _schema + +PROFILES = CatalogCapabilities( + read=sorted([CONSTRAINT_RECORDS_READ, ROW_SCALAR_EXPRESSION]), + ddl=sorted([CONSTRAINT_CREATE, CONSTRAINT_MUTATE]), + write=[CONSTRAINT_ENFORCE], + transaction=[TRANSACTION_DDL], +) + + +class ConstraintProvider(ReadOnlyCatalogInterface): + """Expose one durable authority through ordinary VGI catalog methods.""" + + catalog_name = "constraints" + database_path: ClassVar[str] + profiles: ClassVar[CatalogCapabilities] = PROFILES + catalog_version_frozen = False + catalog_contents_attach_independent = False + + def __init__(self) -> None: + super().__init__() + self.authority = ConstraintCatalog(getattr(self, "database_path", "") or os.environ["VGI_CONSTRAINT_DATABASE"]) + + def catalog_capabilities(self, *, attach_opaque_data: bytes) -> CatalogCapabilities: + return self.profiles + + def catalog_attach(self, **kwargs: Any) -> CatalogAttachResult: + result = super().catalog_attach(**kwargs) + attach = AttachOpaqueData(uuid.uuid4().bytes) + return replace( + result, + attach_opaque_data=attach, + attach_opaque_data_required=True, + supports_transactions=True, + supports_catalog_contents=True, + catalog_version_frozen=False, + catalog_version=self.authority.version(attach), + ) + + def catalog_version( + self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes | None, **kwargs: Any + ) -> int: + return self.authority.version(attach_opaque_data, transaction_opaque_data) + + def schemas(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes | None) -> list[SchemaInfo]: + self.authority.version(attach_opaque_data, transaction_opaque_data) + return [ + SchemaInfo(attach_opaque_data=AttachOpaqueData(attach_opaque_data), path=["main"], comment=None, tags={}) + ] + + def schema_get(self, *, path: list[str], **kwargs: Any) -> SchemaInfo | None: + return next((item for item in self.schemas(**kwargs) if item.path == path), None) + + @staticmethod + def _info(record: dict[str, Any]) -> TableInfo: + snapshot = ConstraintCatalog._readback(record) + return TableInfo( + name=snapshot.table.name, + schema_path=snapshot.table.schema_path, + columns=SerializedSchema(_schema(record).serialize().to_pybytes()), + constraints=snapshot.constraints, + definition_revision=snapshot.definition_revision, + comment=None, + tags={}, + ) + + def _snapshot_infos(self, attach: bytes, tx: bytes | None) -> tuple[int, list[TableInfo]]: + with closing(sqlite3.connect(self.authority.database_path)) as db: + version, data = self.authority._snapshot(db, attach, tx) + return version, [self._info(record) for record in data.values()] + + def table_get( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + schema_path: list[str], + name: str, + at_unit: str | None = None, + at_value: str | None = None, + ) -> TableInfo | None: + if at_unit is not None or at_value is not None: + raise ValueError("constraint reference does not support time travel") + table = CatalogObjectRef(kind="table", schema_path=schema_path, name=name) + with closing(sqlite3.connect(self.authority.database_path)) as db: + _, data = self.authority._snapshot(db, attach_opaque_data, transaction_opaque_data) + try: + record = self.authority._lookup(data, table) + except ValueError: + return None + return self._info(record) + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[SchemaObjectType.TABLE], + ) -> Sequence[TableInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[SchemaObjectType.VIEW], + ) -> Sequence[ViewInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[ + SchemaObjectType.SCALAR_FUNCTION, + SchemaObjectType.TABLE_FUNCTION, + SchemaObjectType.AGGREGATE_FUNCTION, + ], + ) -> Sequence[FunctionInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[SchemaObjectType.SCALAR_MACRO, SchemaObjectType.TABLE_MACRO], + ) -> Sequence[MacroInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[SchemaObjectType.INDEX], + ) -> Sequence[IndexInfo]: ... + + def schema_contents( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + path: list[str], + type: SchemaObjectType, + ) -> Sequence[TableInfo | ViewInfo | FunctionInfo | MacroInfo | IndexInfo]: + _, infos = self._snapshot_infos(attach_opaque_data, transaction_opaque_data) + return [info for info in infos if info.schema_path == path] if type == SchemaObjectType.TABLE else [] + + def catalog_contents(self, *, attach_opaque_data: bytes, if_none_match: str | None = None) -> CatalogContentsResult: + version, infos = self._snapshot_infos(attach_opaque_data, None) + return CatalogContentsResult( + schemas=[ + SchemaContentsInfo( + schema=self.schemas(attach_opaque_data=attach_opaque_data, transaction_opaque_data=None)[0], + tables=infos, + ) + ], + etag=str(version), + ) + + def table_create( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + schema_path: list[str], + name: str, + columns: bytes, + on_conflict: OnConflict, + constraint_definitions: list[bytes], + **kwargs: Any, + ) -> None: + if ( + schema_path != ["main"] + or on_conflict != OnConflict.ERROR + or any(value is not None for value in kwargs.values()) + ): + raise ValueError("reference supports main-schema ERROR-on-conflict creation without other table options") + self.authority.create( + attach_opaque_data, + CatalogObjectRef(kind="table", schema_path=schema_path, name=name), + pa.ipc.read_schema(pa.py_buffer(columns)), + [ConstraintDefinition.deserialize_from_bytes(value) for value in constraint_definitions], + tx=transaction_opaque_data, + ) + + def table_constraints_apply(self, request: ConstraintsApplyRequest) -> ConstraintSnapshot: + return self.authority.apply(request) + + def table_drop( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + schema_path: list[str], + name: str, + cascade: bool = False, + ignore_not_found: bool = False, + ) -> None: + table = CatalogObjectRef(kind="table", schema_path=schema_path, name=name) + if ( + ignore_not_found + and self.table_get( + attach_opaque_data=attach_opaque_data, + transaction_opaque_data=transaction_opaque_data, + schema_path=schema_path, + name=name, + ) + is None + ): + return + self.authority.drop_table(attach_opaque_data, table, cascade=cascade, tx=transaction_opaque_data) + + def _not_null(self, add: bool, **kwargs: Any) -> None: + path = kwargs["column_path"] + if len(path) != 1: + raise ValueError("nested NOT_NULL is outside this reference") + table = CatalogObjectRef(kind="table", schema_path=kwargs["schema_path"], name=kwargs["name"]) + attach, tx = kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"] + with closing(sqlite3.connect(self.authority.database_path)) as db: + _, data = self.authority._snapshot(db, attach, tx) + record = self.authority._lookup(data, table) + snapshot = self.authority._readback(record) + schema = _schema(record) + infos = snapshot.constraint_infos() + if path[0] not in schema.names: + raise ValueError("unknown constraint column") + matches = [ + info + for info in infos + if info.definition.kind == ConstraintKind.NOT_NULL and info.definition.column_paths == [path] + ] + if add: + if not schema.field(path[0]).nullable: + return + changes = [ + ConstraintChange( + operation=ConstraintOperation.ADD, + definition=ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[path]), + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + ) + ] + else: + if any( + info.definition.kind == ConstraintKind.PRIMARY_KEY and path in info.definition.column_paths + for info in infos + ): + raise ValueError("cannot drop primary-key-implied NOT_NULL") + if not matches: + return + changes = [ + ConstraintChange(operation=ConstraintOperation.DROP, ref=info.ref, cascade=False) for info in matches + ] + self.authority.apply( + ConstraintsApplyRequest( + attach_opaque_data=attach, + transaction_opaque_data=tx, + table=snapshot.table, + expected_revision=snapshot.definition_revision, + changes=[change.serialize_to_bytes() for change in changes], + ) + ) + + def table_not_null_set(self, **kwargs: Any) -> None: + self._not_null(True, **kwargs) + + def table_not_null_drop(self, **kwargs: Any) -> None: + self._not_null(False, **kwargs) + + def catalog_transaction_begin(self, *, attach_opaque_data: bytes) -> TransactionOpaqueData: + return TransactionOpaqueData(self.authority.begin(attach_opaque_data)) + + def catalog_transaction_commit(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes) -> None: + self.authority.commit(attach_opaque_data, transaction_opaque_data) + + def catalog_transaction_rollback(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes) -> None: + self.authority.rollback(attach_opaque_data, transaction_opaque_data) + + +class ConstraintWorker(Worker): + """Public worker exposing only the qualified catalog operations.""" + + catalog_interface = ConstraintProvider + + +if __name__ == "__main__": + ConstraintWorker.main() diff --git a/tests/catalog/constraints_reference.py b/tests/catalog/constraints_reference.py new file mode 100644 index 00000000..18c3a26b --- /dev/null +++ b/tests/catalog/constraints_reference.py @@ -0,0 +1,582 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103, D107 + +"""Durable, directly invoked canonical-constraint authority. + +This fixture deliberately does not advertise a public catalog capability or +replace legacy TableInfo. SQLite snapshots contain both Arrow data and canonical +constraint IPC. Every candidate is fully validated before publication. Its +closed scope is top-level boolean, signed-integer and string columns, immediate +ENFORCED/ALL_ROWS constraints, and MATCH SIMPLE NO_ACTION/RESTRICT foreign keys. +DuckDB binds/evaluates CHECKs through the shared deterministic expression +validator; the fixture never interprets CHECK text itself. Replacing an existing +FK preserves its exact referenced key; changing that target is explicitly outside +this reference and requires dropping/recreating the FK in separate validated +operations. Default/generated column semantics are outside this reference. +""" + +from __future__ import annotations + +import base64 +import json +import sqlite3 +import uuid +from contextlib import closing +from dataclasses import replace +from typing import Any + +import duckdb +import pyarrow as pa + +from vgi.catalog.constraints import ( + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, + ConstraintValidation, + ForeignKeyMatch, + ReferentialAction, +) +from vgi.catalog.dependencies import CatalogObjectRef + +from .expression_reference import evaluate_expression + + +def _fold(value: str) -> str: + return value.translate(str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")) + + +def _table_key(ref: CatalogObjectRef) -> str: + ref.__post_init__() + if ref.kind != "table": + raise ValueError("expected a table identity") + return json.dumps([[_fold(part) for part in ref.schema_path], _fold(ref.name)]) + + +def _b64(value: bytes) -> str: + return base64.b64encode(value).decode("ascii") + + +def _unb64(value: str) -> bytes: + return base64.b64decode(value, validate=True) + + +def _quoted(value: str) -> str: + return '"' + value.replace('"', '""') + '"' + + +def _encode_rows(rows: pa.Table) -> str: + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, rows.schema) as writer: + writer.write_table(rows) + return _b64(sink.getvalue().to_pybytes()) + + +def _infos(record: dict[str, Any]) -> list[ConstraintInfo]: + return [ConstraintInfo.deserialize_from_bytes(_unb64(value)) for value in record["constraints"]] + + +def _set_infos(record: dict[str, Any], infos: list[ConstraintInfo]) -> None: + record["constraints"] = [_b64(info.serialize_to_bytes()) for info in infos] + + +def _table_ref(record: dict[str, Any]) -> CatalogObjectRef: + return CatalogObjectRef.deserialize_from_bytes(_unb64(record["table"])) + + +def _base_schema(record: dict[str, Any]) -> pa.Schema: + return pa.ipc.read_schema(pa.py_buffer(_unb64(record["schema"]))) + + +def _rows(record: dict[str, Any]) -> pa.Table: + with pa.ipc.open_stream(_unb64(record["rows"])) as reader: + return reader.read_all() + + +def _schema(record: dict[str, Any]) -> pa.Schema: + required = { + path[0] + for info in _infos(record) + if info.definition.kind in (ConstraintKind.NOT_NULL, ConstraintKind.PRIMARY_KEY) + for path in info.definition.column_paths + } + base = _base_schema(record) + return pa.schema( + [field.with_nullable(field.name not in required) for field in base], + metadata=None if base.metadata is None else {key: value for key, value in (base.metadata).items()}, + ) + + +def _touch(record: dict[str, Any]) -> None: + record["revision"] = _b64(uuid.uuid4().bytes) + + +class ConstraintCatalog: + """Serial SQLite publication with persisted transaction-local snapshots. + + Each direct mutation is an atomic statement: a failed candidate preserves + earlier transaction work. Tokens are owner-bound; concurrent committed + changes cause snapshot-commit conflicts. This is reference evidence, not a + negotiated general statement/savepoint implementation. + """ + + def __init__(self, database_path: str) -> None: + self.database_path = database_path + with closing(sqlite3.connect(database_path)) as db, db: + db.execute("CREATE TABLE IF NOT EXISTS catalog (id INTEGER PRIMARY KEY, version INTEGER, data TEXT)") + db.execute("INSERT OR IGNORE INTO catalog VALUES (1, 1, '{}')") + db.execute( + "CREATE TABLE IF NOT EXISTS transactions " + "(id BLOB PRIMARY KEY, owner BLOB, base INTEGER, version INTEGER, data TEXT, status TEXT)" + ) + + @staticmethod + def _snapshot(db: sqlite3.Connection, attach: bytes, tx: bytes | None) -> tuple[int, dict[str, Any]]: + if not attach: + raise ValueError("attachment identity is required") + if tx is None: + version, data = db.execute("SELECT version, data FROM catalog WHERE id=1").fetchone() + else: + row = db.execute( + "SELECT version, data, status FROM transactions WHERE id=? AND owner=?", (tx, attach) + ).fetchone() + if row is None or row[2] != "open": + raise ValueError("transaction is not open for this owner") + version, data, _ = row + return version, json.loads(data) + + def _after_write(self) -> None: + """Fault-injection point after SQLite writes and before commit.""" + + def _publish(self, db: sqlite3.Connection, version: int, data: dict[str, Any], tx: bytes | None) -> None: + self._validate(data) + if tx is None: + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version + 1, json.dumps(data))) + else: + db.execute("UPDATE transactions SET version=?, data=? WHERE id=?", (version + 1, json.dumps(data), tx)) + self._after_write() + + def begin(self, attach: bytes) -> bytes: + tx = uuid.uuid4().bytes + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, None) + db.execute( + "INSERT INTO transactions VALUES (?, ?, ?, ?, ?, 'open')", + (tx, attach, version, version, json.dumps(data)), + ) + return tx + + def _finish(self, attach: bytes, tx: bytes, status: str) -> None: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + row = db.execute( + "SELECT base, version, data, status FROM transactions WHERE id=? AND owner=?", (tx, attach) + ).fetchone() + if row is None: + raise ValueError("unknown transaction") + base, version, data, previous = row + if previous == status: + return + if previous != "open": + raise ValueError("transaction is terminal") + if status == "committed" and version != base: + current = db.execute("SELECT version FROM catalog WHERE id=1").fetchone()[0] + if current != base: + raise ValueError("transaction conflict") + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version, data)) + db.execute("UPDATE transactions SET status=? WHERE id=?", (status, tx)) + self._after_write() + + def commit(self, attach: bytes, tx: bytes) -> None: + self._finish(attach, tx, "committed") + + def rollback(self, attach: bytes, tx: bytes) -> None: + self._finish(attach, tx, "rolled_back") + + def version(self, attach: bytes, tx: bytes | None = None) -> int: + with closing(sqlite3.connect(self.database_path)) as db: + return self._snapshot(db, attach, tx)[0] + + @staticmethod + def _lookup(data: dict[str, Any], table: CatalogObjectRef) -> dict[str, Any]: + record = data.get(_table_key(table)) + if record is None: + raise ValueError("table does not exist") + assert isinstance(record, dict) + return record + + @staticmethod + def _readback(record: dict[str, Any]) -> ConstraintSnapshot: + return ConstraintSnapshot( + table=_table_ref(record), + definition_revision=_unb64(record["revision"]), + constraints=[info.serialize_to_bytes() for info in _infos(record)], + ) + + def get(self, attach: bytes, table: CatalogObjectRef, tx: bytes | None = None) -> ConstraintSnapshot: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach, tx) + return self._readback(self._lookup(data, table)) + + def columns(self, attach: bytes, table: CatalogObjectRef, tx: bytes | None = None) -> pa.Schema: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach, tx) + return _schema(self._lookup(data, table)) + + def rows(self, attach: bytes, table: CatalogObjectRef, tx: bytes | None = None) -> pa.Table: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach, tx) + record = self._lookup(data, table) + return pa.Table.from_arrays(_rows(record).columns, schema=_schema(record)) + + @staticmethod + def _column_names(schema: pa.Schema, paths: list[list[str]]) -> list[str]: + names = [] + for path in paths: + if len(path) != 1: + raise ValueError("nested constraint paths are outside this reference") + if path[0] not in schema.names: + raise ValueError("unknown constraint column") + names.append(path[0]) + return names + + @staticmethod + def _check_columns(columns: pa.Schema) -> None: + if not columns.names or len({_fold(name) for name in columns.names}) != len(columns.names): + raise ValueError("expected nonempty, unambiguous table columns") + for field in columns: + if not field.name or "\x00" in field.name: + raise ValueError("invalid column name") + if not ( + pa.types.is_boolean(field.type) + or pa.types.is_signed_integer(field.type) + or pa.types.is_string(field.type) + ): + raise ValueError("unsupported constraint reference column type") + if any( + key.startswith((b"vgi.logical_type.", b"ARROW:extension:", b"vgi.column.")) + or key in (b"default", b"generated", b"generation") + for key in field.metadata or {} + ): + raise ValueError("column semantics are outside this reference") + + def _new_info( + self, + data: dict[str, Any], + table: CatalogObjectRef, + definition: ConstraintDefinition, + *, + ref: CatalogObjectRef | None = None, + previous: ConstraintInfo | None = None, + ) -> ConstraintInfo: + definition.__post_init__() + referenced = None + if definition.foreign_key is not None: + fk = definition.foreign_key + parent = self._lookup(data, fk.referenced_table) + canonical = _table_ref(parent) + definition = replace(definition, foreign_key=replace(fk, referenced_table=canonical)) + if previous is not None and previous.referenced_constraint is not None: + previous_fk = previous.definition.foreign_key + assert previous_fk is not None + if ( + previous_fk.referenced_table != canonical + or previous_fk.referenced_column_paths != fk.referenced_column_paths + ): + raise ValueError("changing an existing foreign-key target is outside this reference") + referenced = previous.referenced_constraint + else: + candidates = [ + item.ref + for item in _infos(parent) + if item.definition.kind in (ConstraintKind.UNIQUE, ConstraintKind.PRIMARY_KEY) + and item.definition.column_paths == fk.referenced_column_paths + ] + if not candidates: + raise ValueError("foreign key requires an exact referenced UNIQUE/PRIMARY key") + referenced = candidates[0] + return ConstraintInfo( + ref=ref + or CatalogObjectRef( + kind="constraint", schema_path=table.schema_path, owner_table=table.name, name=uuid.uuid4().hex + ), + definition=definition, + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + referenced_constraint=referenced, + ) + + def create( + self, + attach: bytes, + table: CatalogObjectRef, + columns: pa.Schema, + definitions: list[ConstraintDefinition], + *, + tx: bytes | None = None, + ) -> ConstraintSnapshot: + self._check_columns(columns) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, tx) + key = _table_key(table) + if key in data: + raise ValueError("table already exists") + base = pa.schema( + [field.with_nullable(True) for field in columns], + metadata=None + if columns.metadata is None + else {key: value for key, value in (columns.metadata).items()}, + ) + record = { + "table": _b64(table.serialize_to_bytes()), + "schema": _b64(base.serialize().to_pybytes()), + "constraints": [], + "rows": _encode_rows(pa.Table.from_pylist([], schema=base)), + } + _touch(record) + data[key] = record + # Keys precede FKs so self references resolve without depending on + # caller declaration order. Canonical readback retains that order. + infos = {} + for index, definition in sorted( + enumerate(definitions), key=lambda item: item[1].kind == ConstraintKind.FOREIGN_KEY + ): + infos[index] = self._new_info(data, table, definition) + _set_infos(record, list(infos.values())) + _set_infos(record, [infos[index] for index in range(len(definitions))]) + if not columns.equals(_schema(record), check_metadata=True): + raise ValueError("Arrow nullability must agree with canonical NOT_NULL/PRIMARY constraints") + self._publish(db, version, data, tx) + return self._readback(record) + + def _validate(self, data: dict[str, Any]) -> None: + """Bind definitions and validate the entire candidate data/dependency state.""" + aliases = {key: f"input_{index}" for index, key in enumerate(data)} + all_infos = {info.ref.key(): info for record in data.values() for info in _infos(record)} + with duckdb.connect(config={"enable_external_access": False}) as connection: + for key, record in data.items(): + connection.register(aliases[key], _rows(record)) + for key, record in data.items(): + table, schema, rows = _table_ref(record), _schema(record), _rows(record) + infos = _infos(record) + labels = [ + _fold(info.definition.declared_name) for info in infos if info.definition.declared_name is not None + ] + if len(labels) != len(set(labels)): + raise ValueError("duplicate declared constraint name") + if sum(info.definition.kind == ConstraintKind.PRIMARY_KEY for info in infos) > 1: + raise ValueError("only one PRIMARY key is allowed") + for info in infos: + info.__post_init__() + if info.ref.schema_path != table.schema_path or info.ref.owner_table != table.name: + raise ValueError("constraint identity has the wrong owning table") + if ( + info.enforcement != ConstraintEnforcement.ENFORCED + or info.validation != ConstraintValidation.ALL_ROWS + ): + raise ValueError("only ENFORCED ALL_ROWS is supported") + definition = info.definition + names = self._column_names(schema, definition.column_paths) + relation = _quoted(aliases[key]) + if definition.kind in (ConstraintKind.NOT_NULL, ConstraintKind.PRIMARY_KEY) and any( + rows.column(name).null_count for name in names + ): + raise ValueError("NOT_NULL constraint failed") + if definition.kind in (ConstraintKind.UNIQUE, ConstraintKind.PRIMARY_KEY): + columns = ", ".join(_quoted(name) for name in names) + present = " AND ".join(f"{_quoted(name)} IS NOT NULL" for name in names) + if connection.execute( + f"SELECT 1 FROM {relation} WHERE {present} GROUP BY {columns} HAVING count(*) > 1 LIMIT 1" + ).fetchone(): + raise ValueError("UNIQUE constraint failed") + if definition.kind == ConstraintKind.CHECK: + assert definition.check_expression is not None + checked = evaluate_expression( + definition.check_expression, rows, table.schema_path, role="check" + ) + if not pa.types.is_boolean(checked.type): + raise ValueError("CHECK expression must return BOOLEAN") + if any(value is False for value in checked.to_pylist()): + raise ValueError("CHECK constraint failed") + if definition.kind == ConstraintKind.FOREIGN_KEY: + fk = definition.foreign_key + assert fk is not None and info.referenced_constraint is not None + if ( + fk.match != ForeignKeyMatch.SIMPLE + or fk.on_update not in (ReferentialAction.NO_ACTION, ReferentialAction.RESTRICT) + or fk.on_delete not in (ReferentialAction.NO_ACTION, ReferentialAction.RESTRICT) + ): + raise ValueError("unsupported foreign-key match or action") + parent = self._lookup(data, fk.referenced_table) + target = all_infos.get(info.referenced_constraint.key()) + if target is None or target.definition.kind not in ( + ConstraintKind.UNIQUE, + ConstraintKind.PRIMARY_KEY, + ): + raise ValueError("referenced key dependency is missing") + if ( + target.ref.schema_path != fk.referenced_table.schema_path + or target.ref.owner_table != fk.referenced_table.name + or target.definition.column_paths != fk.referenced_column_paths + ): + raise ValueError("referenced key replacement invalidates foreign key") + parent_schema = _schema(parent) + parent_names = self._column_names(parent_schema, fk.referenced_column_paths) + if any( + schema.field(local).type != parent_schema.field(remote).type + for local, remote in zip(names, parent_names, strict=True) + ): + raise ValueError("foreign-key column types must match exactly") + present = " AND ".join(f"c.{_quoted(name)} IS NOT NULL" for name in names) + equal = " AND ".join( + f"c.{_quoted(local)} = p.{_quoted(remote)}" + for local, remote in zip(names, parent_names, strict=True) + ) + parent_relation = _quoted(aliases[_table_key(fk.referenced_table)]) + if connection.execute( + f"SELECT 1 FROM {relation} c WHERE {present} " + f"AND NOT EXISTS (SELECT 1 FROM {parent_relation} p WHERE {equal}) LIMIT 1" + ).fetchone(): + raise ValueError("FOREIGN_KEY constraint failed") + + @staticmethod + def _drop_constraint(data: dict[str, Any], ref: CatalogObjectRef, cascade: bool) -> None: + dependents = [ + info.ref + for record in data.values() + for info in _infos(record) + if info.referenced_constraint is not None and info.referenced_constraint.key() == ref.key() + ] + if dependents and not cascade: + raise ValueError("referenced key has dependent foreign keys") + for dependent in dependents: + ConstraintCatalog._drop_constraint(data, dependent, True) + for record in data.values(): + infos = _infos(record) + retained = [info for info in infos if info.ref.key() != ref.key()] + if len(retained) != len(infos): + _set_infos(record, retained) + _touch(record) + return + raise ValueError("constraint does not exist") + + def apply(self, request: ConstraintsApplyRequest) -> ConstraintSnapshot: + request.__post_init__() + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, request.attach_opaque_data, request.transaction_opaque_data) + record = self._lookup(data, request.table) + if _unb64(record["revision"]) != request.expected_revision: + raise ValueError("stale definition revision") + original = json.dumps(data, sort_keys=True) + table = _table_ref(record) + for change in request.constraint_changes(): + if change.operation in (ConstraintOperation.ADD, ConstraintOperation.REPLACE) and ( + change.enforcement != ConstraintEnforcement.ENFORCED + or change.validation != ConstraintValidation.ALL_ROWS + ): + raise ValueError("only ENFORCED ALL_ROWS is supported") + infos = _infos(record) + existing = next( + (info for info in infos if change.ref is not None and info.ref.key() == change.ref.key()), None + ) + if change.operation != ConstraintOperation.ADD and existing is None: + raise ValueError("constraint does not exist") + if change.operation == ConstraintOperation.ADD: + assert change.definition is not None + infos.append(self._new_info(data, table, change.definition)) + _set_infos(record, infos) + elif change.operation == ConstraintOperation.DROP: + assert change.ref is not None and change.cascade is not None + self._drop_constraint(data, change.ref, change.cascade) + elif change.operation == ConstraintOperation.RENAME: + assert existing is not None + replacement = replace( + existing, definition=replace(existing.definition, declared_name=change.declared_name) + ) + _set_infos( + record, [replacement if info.ref.key() == existing.ref.key() else info for info in infos] + ) + elif change.operation == ConstraintOperation.REPLACE: + assert existing is not None and change.definition is not None + replacement = self._new_info(data, table, change.definition, ref=existing.ref, previous=existing) + _set_infos( + record, [replacement if info.ref.key() == existing.ref.key() else info for info in infos] + ) + if json.dumps(data, sort_keys=True) != original: + _touch(record) + self._publish(db, version, data, request.transaction_opaque_data) + return self._readback(record) + + def replace_rows(self, attach: bytes, table: CatalogObjectRef, rows: pa.Table, *, tx: bytes | None = None) -> None: + """Replace an entire table atomically, checking incoming and outgoing FKs.""" + rows.validate(full=True) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, tx) + record = self._lookup(data, table) + if not rows.schema.equals(_schema(record), check_metadata=True): + raise ValueError("write schema differs from canonical table schema") + record["rows"] = _encode_rows(pa.Table.from_arrays(rows.columns, schema=_base_schema(record))) + self._publish(db, version, data, tx) + + def drop_table( + self, attach: bytes, table: CatalogObjectRef, *, cascade: bool = False, tx: bytes | None = None + ) -> None: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, tx) + record = self._lookup(data, table) + # Table-owned FKs disappear with their table, including self FKs. + # Remove that owned closure before checking external key blockers. + owned = sorted(_infos(record), key=lambda info: info.definition.kind != ConstraintKind.FOREIGN_KEY) + for info in owned: + # A previous cascade can already have removed a self FK. + if any(current.ref.key() == info.ref.key() for current in _infos(record)): + self._drop_constraint(data, info.ref, cascade) + del data[_table_key(table)] + self._publish(db, version, data, tx) + + def rename_table( + self, attach: bytes, table: CatalogObjectRef, name: str, *, tx: bytes | None = None + ) -> ConstraintSnapshot: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, tx) + record = self._lookup(data, table) + old = _table_ref(record) + renamed = replace(old, name=name) + if _table_key(renamed) in data and _table_key(renamed) != _table_key(old): + raise ValueError("table already exists") + record["table"] = _b64(renamed.serialize_to_bytes()) + for candidate in data.values(): + updated = [] + for info in _infos(candidate): + ref, definition, referenced = info.ref, info.definition, info.referenced_constraint + if ref.schema_path == old.schema_path and ref.owner_table == old.name: + ref = replace(ref, owner_table=name) + if ( + referenced is not None + and referenced.schema_path == old.schema_path + and referenced.owner_table == old.name + ): + referenced = replace(referenced, owner_table=name) + if definition.foreign_key is not None and _table_key( + definition.foreign_key.referenced_table + ) == _table_key(old): + definition = replace( + definition, foreign_key=replace(definition.foreign_key, referenced_table=renamed) + ) + updated.append(replace(info, ref=ref, definition=definition, referenced_constraint=referenced)) + if updated != _infos(candidate) or candidate is record: + _set_infos(candidate, updated) + _touch(candidate) + del data[_table_key(old)] + data[_table_key(renamed)] = record + self._publish(db, version, data, tx) + return self._readback(record) diff --git a/tests/catalog/dependencies_native.py b/tests/catalog/dependencies_native.py new file mode 100644 index 00000000..9c18f099 --- /dev/null +++ b/tests/catalog/dependencies_native.py @@ -0,0 +1,10 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +"""Run the durable metadata dependency reference for native SQL tests.""" + +import os + +from .dependencies_reference import DependencyCatalog, DependencyWorker + +if __name__ == "__main__": + DependencyCatalog.database_path = os.environ["VGI_DEPENDENCY_REFERENCE_DB"] + DependencyWorker.main() diff --git a/tests/catalog/dependencies_reference.py b/tests/catalog/dependencies_reference.py new file mode 100644 index 00000000..1a7bdeb9 --- /dev/null +++ b/tests/catalog/dependencies_reference.py @@ -0,0 +1,655 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103 +"""Durable dependency authority for metadata-only tables, generated columns and views. + +The view binder deliberately accepts only SELECT * FROM one table/view, with +an optional single raw schema identifier. Other SQL is rejected, never treated +as dependency-free. This reference does not execute or advertise table writes. +""" + +from __future__ import annotations + +import base64 +import json +import sqlite3 +from contextlib import closing +from dataclasses import replace +from typing import Any, Literal, cast + +import duckdb +import pyarrow as pa + +from vgi.catalog import ( + AttachOpaqueData, + CatalogObjectRef, + DependencySnapshot, + ObjectDependency, + OnConflict, + SchemaInfo, + TableInfo, + ViewInfo, +) +from vgi.catalog.capabilities import ( + CATALOG_DEPENDENCIES_READ, + COLUMN_NESTED_COMMENT, + COLUMN_NESTED_DROP, + COLUMN_NESTED_RENAME, + COLUMN_NESTED_TYPE, + ROW_SCALAR_EXPRESSION, + SCHEMA_LIFECYCLE, + TRANSACTION_DDL, +) +from vgi.catalog.catalog_interface import CatalogContentsResult, SchemaContentsInfo, SchemaObjectType, SerializedSchema +from vgi.catalog.sql import column_expressions +from vgi.column_path import column_definition, column_path_key, replace_column_field, resolve_column_path +from vgi.schema_path import schema_path_key +from vgi.worker import Worker + +from .expression_reference import evaluate_expression, validate_expression +from .schema_options_reference import DEFINITIONS, PROFILES, SchemaOptionsCatalog, _key +from .typing_helpers import present + +DEPENDENCY_PROFILES = replace( + PROFILES, + read=sorted([*PROFILES.read, CATALOG_DEPENDENCIES_READ, ROW_SCALAR_EXPRESSION]), + ddl=sorted( + [ + *PROFILES.ddl, + SCHEMA_LIFECYCLE, + COLUMN_NESTED_COMMENT, + COLUMN_NESTED_DROP, + COLUMN_NESTED_RENAME, + COLUMN_NESTED_TYPE, + ] + ), +) + + +def schema_ref(path: list[str]) -> CatalogObjectRef: + schema_path_key(path) + return CatalogObjectRef(kind="schema", schema_path=path[:-1], name=path[-1]) + + +def object_key(ref: CatalogObjectRef) -> str: + ref.__post_init__() + if ref.kind == "schema": + return _key([*ref.schema_path, ref.name]) + # This reference follows the existing schema reference's case-insensitive + # key policy. The public graph returns original canonical spelling. + return "object:" + json.dumps([ref.kind, schema_path_key(ref.schema_path), ref.name.lower(), ref.owner_table]) + + +def edge( + dependent: CatalogObjectRef, + subject: CatalogObjectRef, + kind: str = "requires", + *, + blocks_alter: bool = False, + subject_column_paths: list[list[str]] | None = None, + dependent_column_path: list[str] | None = None, +) -> ObjectDependency: + return ObjectDependency( + dependent=dependent.serialize_to_bytes(), + subject=subject.serialize_to_bytes(), + kind=kind, + blocks_alter=blocks_alter, + subject_column_paths=subject_column_paths, + dependent_column_path=dependent_column_path, + ) + + +def _read_schema(payload: bytes) -> pa.Schema: + reader = pa.BufferReader(payload) + schema = pa.ipc.read_schema(reader) # type: ignore[arg-type] # Arrow accepts a file-like reader; retain consumed-byte check. + if reader.tell() != len(payload): + raise ValueError("table schema contains trailing bytes") + return schema + + +def _schema(info: TableInfo | ViewInfo) -> pa.Schema: + assert isinstance(info, TableInfo) + return _read_schema(info.columns) + + +def _primitive(dtype: pa.DataType) -> bool: + return pa.types.is_signed_integer(dtype) or pa.types.is_boolean(dtype) or pa.types.is_string(dtype) + + +def _generated(schema: pa.Schema, owner: list[str]) -> dict[str, list[list[str]]]: + """Bind the qualified virtual DAG and derive every exact column edge.""" + definitions = {} + + def fields(items: Any, parent: list[str]) -> None: + names = [item.name.casefold() for item in items] + if len(set(names)) != len(names): + raise ValueError("duplicate or case-ambiguous column names") + for item in items: + column_path_key([item.name]) + if not item.nullable: + raise ValueError("metadata-only reference cannot claim NOT NULL enforcement") + metadata = item.metadata or {} + if any(key.startswith((b"ARROW:extension:", b"vgi.logical_type.")) for key in metadata): + raise ValueError("reference does not qualify logical or extension types") + if any(key in metadata for key in (b"default", b"generated_expression", b"collation", b"compression")): + raise ValueError("legacy or unsupported column semantics") + exprs = column_expressions(item) + if exprs.default is not None: + raise ValueError("reference does not implement default expressions") + if exprs.generated is not None: + if parent or exprs.generation != "virtual" or not _primitive(item.type): + raise ValueError("reference supports top-level primitive virtual generated columns only") + definitions[item.name] = exprs.generated + if pa.types.is_struct(item.type): + if not len(item.type): + raise ValueError("empty structs are unsupported") + fields(item.type, [*parent, item.name]) + elif not _primitive(item.type): + raise ValueError("reference supports ordinary structs and primitive signed integer/bool/string fields") + + fields(schema, []) + if not definitions: + return {} + canonical = {item.name.casefold(): item for item in schema} + dependencies = {} + with duckdb.connect(config={"enable_external_access": False}) as con: + for name, expression in definitions.items(): + references = validate_expression(con, expression, owner, role="generated") + paths = [] + for reference in references: + field = canonical.get(reference.casefold()) + if field is None or not _primitive(field.type): + raise ValueError("generated expression references an unknown or unsupported column") + paths.append([field.name]) + dependencies[name] = [list(path) for path in sorted({tuple(path) for path in paths})] + pending = set(definitions) + # Bind with actual Arrow types, even for a metadata-only table with no rows. + base = pa.schema([item for item in schema if item.name not in definitions and _primitive(item.type)]) + candidate = pa.Table.from_pylist([], schema=base) + while pending: + ready = sorted(name for name in pending if not {path[0] for path in dependencies[name]} & pending) + if not ready: + raise ValueError("generated column dependency cycle") + for name in ready: + result = evaluate_expression(definitions[name], candidate, owner, role="generated") + if result.type != schema.field(name).type: + raise ValueError("generated expression result type does not match declared column type") + candidate = candidate.append_column(name, result) + pending.difference_update(ready) + return dependencies + + +class DependencyCatalog(SchemaOptionsCatalog): + """Persist authoritative dependency and metadata snapshots atomically.""" + + catalog_name = "dependencies" + profiles = DEPENDENCY_PROFILES + + def _objects(self, data: dict[str, str]) -> dict[str, CatalogObjectRef]: + result = {} + for key, value in data.items(): + if key.startswith("["): + result[key] = schema_ref(self._decode(value, AttachOpaqueData(b"")).path) + else: + result[key] = CatalogObjectRef.deserialize_from_bytes(base64.b64decode(json.loads(value)["ref"])) + return result + + def _graph(self, version: int, data: dict[str, str]) -> DependencySnapshot: + objects = self._objects(data) + dependencies = [] + schemas = {} + for key, ref in objects.items(): + if ref.schema_path: + dependencies.append(edge(ref, objects[_key(ref.schema_path)])) + if ref.kind == "view": + for subject in json.loads(data[key])["subjects"]: + dependencies.append(edge(ref, objects[subject], blocks_alter=True)) + if ref.kind == "table": + info = self._info(data[key]) + assert isinstance(info, TableInfo) + schema = _schema(info) + schemas[ref.key()] = schema + for name, paths in _generated(schema, ref.schema_path).items(): + if paths: + dependencies.append( + edge(ref, ref, blocks_alter=True, subject_column_paths=paths, dependent_column_path=[name]) + ) + graph = DependencySnapshot( + catalog_version=version, + objects=[ref.serialize_to_bytes() for ref in objects.values()], + dependencies=[item.serialize_to_bytes() for item in dependencies], + ) + graph.validate_columns(schemas) + return graph + + def _column_schemas(self, data: dict[str, str]) -> dict[Any, pa.Schema]: + return { + ref.key(): _schema(self._info(data[key])) for key, ref in self._objects(data).items() if ref.kind == "table" + } + + def _require_transaction_profile(self, transaction: bytes | None) -> None: + if transaction is not None and TRANSACTION_DDL not in self.profiles.transaction: + raise ValueError("semantic mutation requires transaction DDL profile") + + def catalog_dependencies(self, **kwargs: Any) -> DependencySnapshot: + with closing(sqlite3.connect(self.database_path)) as db: + return self._graph(*self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"])) + + def schemas(self, **kwargs: Any) -> list[SchemaInfo]: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + return [self._decode(v, kwargs["attach_opaque_data"]) for k, v in data.items() if k.startswith("[")] + + @staticmethod + def _info(value: str) -> TableInfo | ViewInfo: + item = json.loads(value) + cls = TableInfo if item["kind"] == "table" else ViewInfo + return cls.deserialize_from_bytes(base64.b64decode(item["info"])) + + def _get(self, kind: str, kwargs: dict[str, Any]) -> TableInfo | ViewInfo | None: + ref = CatalogObjectRef(kind=kind, schema_path=kwargs["schema_path"], name=kwargs["name"]) + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + value = data.get(object_key(ref)) + return self._info(value) if value is not None else None + + def table_get(self, **kwargs: Any) -> TableInfo | None: + info = self._get("table", kwargs) + assert info is None or isinstance(info, TableInfo) + return info + + def view_get(self, **kwargs: Any) -> ViewInfo | None: + info = self._get("view", kwargs) + assert info is None or isinstance(info, ViewInfo) + return info + + def schema_contents(self, **kwargs: Any) -> Any: + expected = {SchemaObjectType.TABLE: "table", SchemaObjectType.VIEW: "view"}.get(kwargs["type"]) + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + return [ + self._info(data[k]) + for k, r in self._objects(data).items() + if r.kind == expected and schema_path_key(r.schema_path) == schema_path_key(kwargs["path"]) + ] + + def catalog_contents(self, **kwargs: Any) -> CatalogContentsResult: + # All related metadata comes from the same SQLite snapshot. + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, kwargs["attach_opaque_data"], None) + objects = self._objects(data) + schemas = [] + for key, ref in objects.items(): + if ref.kind != "schema": + continue + info = self._decode(data[key], kwargs["attach_opaque_data"]) + items = [ + (r.kind, self._info(data[k])) + for k, r in objects.items() + if r.kind != "schema" and _key(r.schema_path) == key + ] + tables: list[TableInfo] = [] + views: list[ViewInfo] = [] + for kind, item in items: + if kind == "table": + assert isinstance(item, TableInfo) + tables.append(item) + elif kind == "view": + assert isinstance(item, ViewInfo) + views.append(item) + schemas.append(SchemaContentsInfo(schema=info, tables=tables, views=views)) + return CatalogContentsResult(schemas=schemas, etag="dependency-reference") + + def _authorize_drop(self, ref: CatalogObjectRef) -> None: + """Provider authorization hook; tests deny one member of a cascade.""" + + def _after_write(self) -> None: + """Failure injection after SQLite UPDATE, before transaction commit.""" + + def _publish(self, db: sqlite3.Connection, version: int, data: dict[str, str], tx: bytes | None) -> None: + self._require_transaction_profile(tx) + self._graph(version + 1, data) # Never persist dangling references. + if tx is None: + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version + 1, json.dumps(data))) + else: + db.execute("UPDATE transactions SET version=?, data=? WHERE id=?", (version + 1, json.dumps(data), tx)) + self._after_write() + + def _drop_keys(self, version: int, data: dict[str, str], target: CatalogObjectRef, cascade: bool) -> list[str]: + canonical = self._objects(data)[object_key(target)] + drops = self._graph(version, data).drop_set(canonical, cascade=cascade) + for ref in drops: + self._authorize_drop(ref) + return [object_key(ref) for ref in drops] + + def _mutate(self, attach: Any, tx: Any, path: list[str], **kwargs: Any) -> None: + ref = schema_ref(path) + key = object_key(ref) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, tx) + if kwargs["create"]: + options = DEFINITIONS.create(kwargs.get("options")) + conflict = OnConflict(kwargs["on_conflict"]) + if ref.schema_path and _key(ref.schema_path) not in data: + raise ValueError("parent schema does not exist") + if ref.schema_path: + parent = self._decode(data[_key(ref.schema_path)], AttachOpaqueData(b"")).path + path = [*parent, ref.name] + if key in data: + if conflict == OnConflict.IGNORE: + return + if conflict != OnConflict.REPLACE: + raise ValueError("schema already exists") + for drop in self._drop_keys(version, data, ref, False): + del data[drop] + info = SchemaInfo( + attach_opaque_data=AttachOpaqueData(b""), + path=path, + comment=kwargs.get("comment"), + tags=kwargs.get("tags") or {}, + options=options, + ) + else: + if key not in data: + raise ValueError("schema does not exist") + info = self._decode(data[key], AttachOpaqueData(b"")) + info = replace(info, options=DEFINITIONS.apply(present(info.options), kwargs["changes"])) + data[key] = self._encode(info) + self._publish(db, version, data, tx) + + def _drop(self, ref: CatalogObjectRef, **kwargs: Any) -> None: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + if object_key(ref) not in data: + if kwargs["ignore_not_found"]: + return + raise ValueError("catalog object does not exist") + for key in self._drop_keys(version, data, ref, kwargs["cascade"]): + del data[key] + self._publish(db, version, data, kwargs["transaction_opaque_data"]) + + def schema_drop(self, **kwargs: Any) -> None: + self._drop(schema_ref(kwargs["path"]), **kwargs) + + def table_drop(self, **kwargs: Any) -> None: + self._drop(CatalogObjectRef(kind="table", schema_path=kwargs["schema_path"], name=kwargs["name"]), **kwargs) + + def view_drop(self, **kwargs: Any) -> None: + self._drop(CatalogObjectRef(kind="view", schema_path=kwargs["schema_path"], name=kwargs["name"]), **kwargs) + + @staticmethod + def _bind_view(definition: str, path: list[str], data: dict[str, str]) -> tuple[str, TableInfo | ViewInfo]: + with duckdb.connect(config={"enable_external_access": False}) as con: + row = con.execute("SELECT json_serialize_sql(?)", [definition]).fetchone() + assert row is not None + parsed = json.loads(row[0]) + if parsed.get("error") or len(parsed.get("statements", [])) != 1: + raise ValueError("reference requires SELECT * FROM one relation") + statement = parsed["statements"][0] + node = statement["node"] + source = node.get("from_table") or {} + stars = node.get("select_list", []) + if ( + node["type"] != "SELECT_NODE" + or len(stars) != 1 + or stars[0].get("class") != "STAR" + or any( + stars[0].get(k) + for k in [ + "relation_name", + "exclude_list", + "replace_list", + "columns", + "expr", + "qualified_exclude_list", + "rename_list", + "alias", + ] + ) + or source.get("type") != "BASE_TABLE" + or source.get("catalog_name") + or any(source.get(k) for k in ["sample", "at_clause", "column_name_alias"]) + or any( + node.get(k) + for k in ["modifiers", "where_clause", "group_expressions", "group_sets", "having", "qualify", "sample"] + ) + or node.get("cte_map", {}).get("map") + or statement.get("named_param_map") + ): + raise ValueError("reference requires SELECT * FROM one relation") + owner = [source["schema_name"]] if source.get("schema_name") else path + for kind in ["table", "view"]: + key = object_key(CatalogObjectRef(kind=kind, schema_path=owner, name=source["table_name"])) + if key in data: + return key, DependencyCatalog._info(data[key]) + raise ValueError("view dependency does not exist") + + def _create_object(self, kind: str, **kwargs: Any) -> None: + ref = CatalogObjectRef(kind=kind, schema_path=kwargs["schema_path"], name=kwargs["name"]) + key = object_key(ref) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + if _key(ref.schema_path) not in data: + raise ValueError("schema does not exist") + # Resolve the complete owning path to the provider's spelling. + # Case-insensitive request aliases must not leak into graph identity. + ref = replace(ref, schema_path=self._decode(data[_key(ref.schema_path)], AttachOpaqueData(b"")).path) + other_kind = "view" if kind == "table" else "table" + if object_key(replace(ref, kind=other_kind)) in data: + raise ValueError("relation name is already in use") + conflict = OnConflict(kwargs["on_conflict"]) + if key in data: + if conflict == OnConflict.IGNORE: + return + if conflict != OnConflict.REPLACE: + raise ValueError("relation already exists") + self._drop_keys(version, data, ref, False) + subjects = [] + if kind == "table": + if any( + kwargs.get(k) + for k in [ + "constraint_definitions", + "options", + "layout", + ] + ): + raise ValueError("reference does not implement table constraints/options/layout") + _generated(_read_schema(kwargs["columns"]), ref.schema_path) + info: TableInfo | ViewInfo = TableInfo( + name=ref.name, + schema_path=ref.schema_path, + columns=kwargs["columns"], + comment=kwargs.get("comment"), + tags=kwargs.get("tags") or {}, + constraints=[], + ) + else: + subject, bound = self._bind_view(kwargs["definition"], ref.schema_path, data) + if subject == key: + raise ValueError("self-referential views are invalid") + subjects = [subject] + count = ( + len(pa.ipc.read_schema(pa.py_buffer(bound.columns))) + if isinstance(bound, TableInfo) + else len(json.loads(data[subject])["output_names"]) + ) + output_names = kwargs.get("aliases") or ( + pa.ipc.read_schema(pa.py_buffer(bound.columns)).names + if isinstance(bound, TableInfo) + else json.loads(data[subject])["output_names"] + ) + if len(output_names) != count: + raise ValueError("view alias count does not match its output") + info = ViewInfo( + name=ref.name, + schema_path=ref.schema_path, + definition=kwargs["definition"], + aliases=kwargs.get("aliases") or [], + security=kwargs.get("security", "regular"), + comment=kwargs.get("comment"), + tags=kwargs.get("tags") or {}, + column_comments=kwargs.get("column_comments") or {}, + ) + record = dict( + kind=kind, + ref=base64.b64encode(ref.serialize_to_bytes()).decode(), + info=base64.b64encode(info.serialize_to_bytes()).decode(), + subjects=subjects, + ) + if kind == "view": + record["output_names"] = output_names + data[key] = json.dumps(record) + self._publish(db, version, data, kwargs["transaction_opaque_data"]) + + def _authorize_alter(self, ref: CatalogObjectRef, path: list[str] | None, action: str) -> None: + """Provider authorization hook for every semantic target before effects.""" + + @staticmethod + def _store_info( + data: dict[str, str], key: str, info: TableInfo | ViewInfo, ref: CatalogObjectRef | None = None + ) -> None: + record = json.loads(data[key]) + record["info"] = base64.b64encode(info.serialize_to_bytes()).decode() + if ref is not None: + record["ref"] = base64.b64encode(ref.serialize_to_bytes()).decode() + data[key] = json.dumps(record) + + def _alter_table(self, operation: str, **kwargs: Any) -> None: + tx = kwargs["transaction_opaque_data"] + self._require_transaction_profile(tx) + requested = CatalogObjectRef(kind="table", schema_path=kwargs["schema_path"], name=kwargs["name"]) + key = object_key(requested) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, kwargs["attach_opaque_data"], tx) + if key not in data: + if kwargs.get("ignore_not_found"): + return + raise ValueError("table does not exist") + canonical = self._objects(data)[key] + info = self._info(data[key]) + assert isinstance(info, TableInfo) + schema = _schema(info) + path = kwargs.get("column_path") + field = None + if path is not None: + column_path_key(path) + try: + field = resolve_column_path(schema, path) + except ValueError: + if operation == "drop" and kwargs.get("if_column_exists"): + return + raise + if operation == "comment": + if path is None: + info = replace(info, comment=kwargs["comment"]) + else: + assert field is not None + metadata: dict[bytes | str, bytes | str] = { + key: value for key, value in (field.metadata or {}).items() + } + if kwargs["comment"] is None: + metadata.pop(b"comment", None) + else: + metadata[b"comment"] = kwargs["comment"].encode() + schema = replace_column_field(schema, path, field.with_metadata(metadata)) + info = replace(info, columns=SerializedSchema(schema.serialize().to_pybytes())) + self._store_info(data, key, info) + self._publish(db, version, data, tx) + return + graph = self._graph(version, data) + if operation == "drop" and kwargs.get("cascade"): + assert path is not None + self._drop_column_cascade(graph, data, canonical, path) + self._publish(db, version, data, tx) + return + assert operation in ("rename", "type", "drop") + graph.assert_alter_allowed( + canonical, + column_path=path, + action=cast(Literal["rename", "type", "drop"], operation), + schemas=self._column_schemas(data), + ) + self._authorize_alter(canonical, path, operation) + if path is None: + new_ref = replace(canonical, name=kwargs["new_name"]) + new_key = object_key(new_ref) + if new_key != key and (new_key in data or object_key(replace(new_ref, kind="view")) in data): + raise ValueError("relation already exists") + self._store_info(data, key, replace(info, name=new_ref.name), new_ref) + if new_key != key: + data[new_key] = data.pop(key) + else: + assert field is not None + if operation == "rename": + replacement = field.with_name(kwargs["new_column_name"]) + elif operation == "type": + if kwargs.get("expression") is not None: + raise ValueError("metadata-only reference does not execute USING conversions") + replacement_type = column_definition(kwargs["column_definition"], path, type_only=True) + if replacement_type.metadata: + raise ValueError("type carrier cannot alter metadata") + replacement = field.with_type(replacement_type.type) + elif operation == "drop": + replacement = None + else: + raise ValueError("unsupported semantic mutation") + schema = replace_column_field(schema, path, replacement) + self._store_info(data, key, replace(info, columns=SerializedSchema(schema.serialize().to_pybytes()))) + self._publish(db, version, data, tx) + + def _drop_column_cascade( + self, graph: DependencySnapshot, data: dict[str, str], target: CatalogObjectRef, path: list[str] + ) -> None: + # Core planner provides separate object and column closures. Never turn + # one generated-column dependency into deletion of its owning table. + plan = graph.plan_alter( + target, column_path=path, action="drop", cascade=True, schemas=self._column_schemas(data) + ) + for ref in plan.drop_objects: + self._authorize_drop(ref) + for column in plan.drop_columns: + self._authorize_alter(column.table, column.column_path, "drop") + for ref in plan.drop_objects: + del data[object_key(ref)] + for column in sorted(plan.drop_columns, key=lambda item: len(item.column_path), reverse=True): + if object_key(column.table) not in data: + continue + key = object_key(column.table) + info = self._info(data[key]) + assert isinstance(info, TableInfo) + changed = replace_column_field(_schema(info), column.column_path, None) + self._store_info(data, key, replace(info, columns=SerializedSchema(changed.serialize().to_pybytes()))) + + def table_rename(self, **kwargs: Any) -> None: + self._alter_table("rename", **kwargs) + + def table_column_rename(self, **kwargs: Any) -> None: + self._alter_table("rename", **kwargs) + + def table_column_type_change(self, **kwargs: Any) -> None: + self._alter_table("type", **kwargs) + + def table_column_drop(self, **kwargs: Any) -> None: + self._alter_table("drop", **kwargs) + + def table_comment_set(self, **kwargs: Any) -> None: + self._alter_table("comment", **kwargs) + + def table_column_comment_set(self, **kwargs: Any) -> None: + self._alter_table("comment", **kwargs) + + def table_create(self, **kwargs: Any) -> None: + self._create_object("table", **kwargs) + + def view_create(self, **kwargs: Any) -> None: + self._create_object("view", **kwargs) + + +class DependencyWorker(Worker): + """Expose the durable dependency provider through public RPC.""" + + catalog_interface = DependencyCatalog diff --git a/tests/catalog/expression_reference.py b/tests/catalog/expression_reference.py new file mode 100644 index 00000000..fd838a07 --- /dev/null +++ b/tests/catalog/expression_reference.py @@ -0,0 +1,182 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Closed deterministic scalar proof on DuckDB 1.5, not a general SQL executor. + +The local JSON parser output is only inspected here; no syntax tree is sent on +the wire. This reference qualifies the default context and primitive signed +integers, booleans, UTF-8 strings and NULL. Broader types/contexts fail closed. +""" + +from __future__ import annotations + +import json +from typing import Any + +import duckdb +import pyarrow as pa + +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import SqlExpression +from vgi.schema_path import schema_path_key + +_ROLES = {"default", "generated", "check", "using", "layout", "index"} +_TYPES = {"BOOLEAN", "TINYINT", "SMALLINT", "INTEGER", "BIGINT", "VARCHAR", "SQLNULL", "NULL"} + + +def validate_expression( + connection: Any, expr: SqlExpression, owner_path: list[str], *, role: str = "layout" +) -> frozenset[str]: + """Validate one scalar tree and return its row-column dependencies.""" + expr.__post_init__() + if role not in _ROLES: + raise ValueError("unsupported expression evaluation role") + if expr.profile != ROW_SCALAR_EXPRESSION or schema_path_key(expr.schema_path) != schema_path_key(owner_path): + raise ValueError("unsupported expression profile or owner schema") + if expr.evaluation_context != QueryEvaluationContext(): + raise ValueError("reference evaluator supports only the default explicit evaluation context") + parsed = json.loads( + connection.execute("SELECT json_serialize_sql(?)", [f"SELECT ({expr.sql}) AS v FROM input_data"]).fetchone()[0] + ) + if parsed.get("error") or len(parsed.get("statements", [])) != 1: + raise ValueError("expected one scalar expression") + node = parsed["statements"][0]["node"] + source = node.get("from_table", {}) + if ( + node.get("type") != "SELECT_NODE" + or source.get("type") != "BASE_TABLE" + or source.get("table_name") != "input_data" + or source.get("schema_name") + or source.get("catalog_name") + or len(node.get("select_list", [])) != 1 + or node.get("modifiers") + or node.get("cte_map", {}).get("map") + or any( + node.get(key) for key in ("where_clause", "group_expressions", "group_sets", "having", "qualify", "sample") + ) + ): + raise ValueError("expected one row-local scalar expression") + dependencies: set[str] = set() + + def check(expression: dict[str, Any]) -> None: + kind = expression.get("class") + if kind == "COLUMN_REF": + names = expression.get("column_names", []) + # The existing column-edit reference binds nested USING paths to + # its old row schema. Other new roles retain a flat primitive scope. + if role == "default" or not names or (len(names) != 1 and role != "using"): + raise ValueError("column reference is not allowed in this expression scope") + dependencies.add(names[0]) + return + constant_types = _TYPES | ({"DECIMAL", "DOUBLE", "FLOAT"} if role == "using" else set()) + if kind == "CONSTANT" and expression.get("value", {}).get("type", {}).get("id") in constant_types: + return + if kind == "CAST" and expression.get("cast_type", {}).get("id") in _TYPES and not expression.get("try_cast"): + check(expression["child"]) + return + if kind == "COMPARISON" and expression.get("type") in { + "COMPARE_EQUAL", + "COMPARE_NOTEQUAL", + "COMPARE_LESSTHAN", + "COMPARE_GREATERTHAN", + "COMPARE_LESSTHANOREQUALTO", + "COMPARE_GREATERTHANOREQUALTO", + }: + check(expression["left"]) + check(expression["right"]) + return + if kind == "CONJUNCTION" and expression.get("type") in {"CONJUNCTION_AND", "CONJUNCTION_OR"}: + for child in expression["children"]: + check(child) + return + functions = ( + {"+", "-", "*", "%", "abs", "lower", "upper"} + | ({"/"} if role == "using" else set()) + | ({"||"} if role == "generated" else set()) + ) + if kind == "FUNCTION" and expression.get("function_name") in functions: + if ( + expression.get("schema") + or expression.get("catalog") + or expression.get("distinct") + or expression.get("filter") + or expression.get("order_bys", {}).get("orders") + ): + raise ValueError("unsupported scalar function modifiers") + for child in expression["children"]: + check(child) + return + if kind == "OPERATOR" and expression.get("type") in { + "OPERATOR_COALESCE", + "OPERATOR_IS_NULL", + "OPERATOR_IS_NOT_NULL", + "OPERATOR_NOT", + }: + for child in expression["children"]: + check(child) + return + raise ValueError("expression is outside the deterministic row-scalar profile") + + check(node["select_list"][0]) + return frozenset(dependencies) + + +def _primitive(dtype: pa.DataType) -> bool: + return bool( + pa.types.is_signed_integer(dtype) + or pa.types.is_boolean(dtype) + or pa.types.is_string(dtype) + or pa.types.is_null(dtype) + ) + + +def evaluate_expression( + expr: SqlExpression, table: pa.Table, owner_path: list[str], *, role: str +) -> pa.ChunkedArray[Any]: + """Evaluate a qualified row expression without erasing CHECK's NULL values.""" + table.validate(full=True) + for field in table.schema: + if not _primitive(field.type) or any( + key.startswith((b"vgi.logical_type.", b"ARROW:extension:")) for key in (field.metadata or {}) + ): + raise ValueError("reference evaluator requires primitive unannotated input types") + with duckdb.connect(config={"enable_external_access": False}) as connection: + connection.execute("SET TimeZone='UTC'") + connection.execute("SET default_collation='binary'") + connection.execute("SET default_order='asc'") + connection.execute("SET default_null_order='nulls_last'") + connection.execute("SET preserve_insertion_order=true") + connection.register("input_data", table) + validate_expression(connection, expr, owner_path, role=role) + result = connection.execute(f"SELECT ({expr.sql}) AS v FROM input_data").to_arrow_table().column(0) + if not _primitive(result.type) or (role == "check" and not pa.types.is_boolean(result.type)): + raise ValueError("expression result has an unsupported type for its role") + return result + + +def evaluate_generated(table: pa.Table, definitions: dict[str, SqlExpression], owner_path: list[str]) -> pa.Table: + """Resolve a finite generated-column DAG before evaluating its candidate.""" + if set(definitions) & set(table.schema.names): + raise ValueError("generated column names conflict with base columns") + with duckdb.connect(config={"enable_external_access": False}) as connection: + dependencies = { + name: validate_expression(connection, expression, owner_path, role="generated") + for name, expression in definitions.items() + } + known = set(table.schema.names) | set(definitions) + if any(references - known for references in dependencies.values()): + raise ValueError("generated column references an unknown column") + ordered: list[str] = [] + pending = set(definitions) + while pending: + ready = [name for name in definitions if name in pending and not (dependencies[name] & pending)] + if not ready: + raise ValueError("generated column dependency cycle") + ordered.extend(ready) + pending.difference_update(ready) + candidate = table + for name in ordered: + candidate = candidate.append_column( + name, evaluate_expression(definitions[name], candidate, owner_path, role="generated") + ) + return candidate diff --git a/tests/catalog/generated_columns_reference.py b/tests/catalog/generated_columns_reference.py new file mode 100644 index 00000000..6678f046 --- /dev/null +++ b/tests/catalog/generated_columns_reference.py @@ -0,0 +1,172 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103, D106, D107 + +"""Durable read-only physical rows with owner-validated VIRTUAL definitions. + +This fixture reuses the dependency authority's closed scalar binder and SQLite +snapshots. It supplies physical fields only; the native consumer computes the +virtual fields. It does not expose DML or claim semantic column rewrites. +""" + +from __future__ import annotations + +import json +import os +import sqlite3 +from contextlib import closing +from dataclasses import dataclass +from typing import Any + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import OutputCollector + +from vgi.catalog import ( + Catalog, + CatalogObjectRef, + ReadOnlyCatalogInterface, + ScanFunctionResult, + Schema, + SchemaObjectType, +) +from vgi.catalog.sql import ColumnExpressions, column_expressions, with_column_expressions +from vgi.invocation import BindResponse, GlobalInitResponse +from vgi.table_function import BindParams, InitParams, ProcessParams, TableFunctionGenerator +from vgi.worker import Worker + +from .dependencies_reference import DependencyCatalog, _generated, _schema, object_key +from .typing_helpers import present + + +def physical_schema(schema: pa.Schema) -> pa.Schema: + return pa.schema( + [ + with_column_expressions(field, ColumnExpressions()) + for field in schema + if column_expressions(field).generated is None + ], + metadata=None if schema.metadata is None else {key: value for key, value in (schema.metadata).items()}, + ) + + +def _args(params: Any) -> dict[str, Any]: + call = params.bind_call if isinstance(params, BindParams) else params.init_call.bind_call + return dict( + attach_opaque_data=params.attach_opaque_data, + transaction_opaque_data=params.transaction_opaque_data, + **json.loads(present(call.arguments.positional[0]).as_py()), + ) + + +@dataclass +class GeneratedScanState(ArrowSerializableDataclass): + """Remember whether the physical snapshot has been emitted.""" + + emitted: bool = False + + +class GeneratedScan(TableFunctionGenerator[None, GeneratedScanState]): + """Return stored physical columns in their original relative order.""" + + class Meta: + name = "generated_reference_scan" + projection_pushdown = False + filter_pushdown = False + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + info = GeneratedColumnsCatalog().table_get(**_args(params)) + if info is None: + raise ValueError("table does not exist") + return BindResponse(output_schema=physical_schema(_schema(info))) + + @classmethod + def on_init(cls, params: InitParams[None]) -> GlobalInitResponse: + return GlobalInitResponse(max_workers=1) + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> GeneratedScanState: + return GeneratedScanState() + + @classmethod + def process(cls, params: ProcessParams[None], state: GeneratedScanState, out: OutputCollector) -> None: + if state.emitted: + out.finish() + else: + rows = GeneratedColumnsCatalog().physical_rows(**_args(params)) + out.emit(pa.RecordBatch.from_pylist(rows, schema=params.output_schema)) + state.emitted = True + + +CATALOG = Catalog( + name="generated_columns", default_schema="main", schemas=[Schema(path=["main"], functions=[GeneratedScan])] +) + + +class GeneratedColumnsCatalog(DependencyCatalog): + """Keep physical rows and the complete generated definition in one snapshot.""" + + catalog_name = "generated_columns" + catalog = CATALOG + database_path = os.environ.get("VGI_GENERATED_REFERENCE_DB", "") + + def _alter_table(self, operation: str, **kwargs: Any) -> None: + if operation != "comment": + raise NotImplementedError("generated row reference rejects semantic column edits without changes") + super()._alter_table(operation, **kwargs) + + def table_create(self, **kwargs: Any) -> None: + schema = pa.ipc.read_schema(pa.py_buffer(kwargs["columns"])) + _generated(schema, kwargs["schema_path"]) + if not len(physical_schema(schema)): + raise ValueError("generated row reference requires at least one physical column") + super().table_create(**kwargs) + + def seed_rows(self, *, rows: list[dict[str, Any]], **kwargs: Any) -> None: + """Seed physical fixture data atomically, without exposing a write endpoint.""" + ref = CatalogObjectRef(kind="table", schema_path=kwargs["schema_path"], name=kwargs["name"]) + key = object_key(ref) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + record = json.loads(data[key]) + schema = physical_schema(_schema(self._info(data[key]))) + if any(set(row) - set(schema.names) for row in rows): + raise ValueError("seed rows must contain only physical column names") + batch = pa.Table.from_pylist(rows, schema=schema) + batch.validate(full=True) + record["rows"] = [*record.get("rows", []), *batch.to_pylist()] + data[key] = json.dumps(record) + self._publish(db, version, data, kwargs["transaction_opaque_data"]) + + def physical_rows(self, **kwargs: Any) -> list[dict[str, Any]]: + ref = CatalogObjectRef(kind="table", schema_path=kwargs["schema_path"], name=kwargs["name"]) + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + rows: list[dict[str, Any]] = json.loads(data[object_key(ref)]).get("rows", []) + return rows + + def schema_contents(self, **kwargs: Any) -> Any: + if kwargs["type"] in (SchemaObjectType.TABLE, SchemaObjectType.VIEW): + return super().schema_contents(**kwargs) + return ReadOnlyCatalogInterface.schema_contents(self, **kwargs) + + def table_scan_function_get(self, **kwargs: Any) -> ScanFunctionResult: + return ScanFunctionResult( + function_name="generated_reference_scan", + positional_arguments=[ + pa.scalar(json.dumps({"schema_path": kwargs["schema_path"], "name": kwargs["name"]})) + ], + named_arguments={}, + ) + + +class GeneratedColumnsWorker(Worker): + """Host discovery and physical scans through normal signed HTTP or stdio RPC.""" + + catalog_interface = GeneratedColumnsCatalog + catalog = CATALOG + + +if __name__ == "__main__": + GeneratedColumnsWorker.main() diff --git a/tests/catalog/index_protocol_reference.py b/tests/catalog/index_protocol_reference.py new file mode 100644 index 00000000..7b3d6bb3 --- /dev/null +++ b/tests/catalog/index_protocol_reference.py @@ -0,0 +1,498 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103, D106, D107 + +"""Durable finite SQLite indexes with atomic catalog/physical DDL publication. + +One real table has binary-collated INTEGER/TEXT columns. Regular and UNIQUE +indexes admit top-level raw keys only; associations, PRIMARY, expressions, +options and CASCADE reject. Transaction snapshots use private physical tables +and indexes in the same SQLite authority; commits check the catalog revision. +Public scans are available, while writes remain provider-side test operations. +""" + +from __future__ import annotations + +import os +import sqlite3 +import uuid +from collections.abc import Iterator +from contextlib import closing, contextmanager +from dataclasses import dataclass, replace +from typing import Any, ClassVar + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import OutputCollector + +from vgi.catalog import ( + AttachOpaqueData, + Catalog, + CatalogAttachResult, + IndexInfo, + OnConflict, + ReadOnlyCatalogInterface, + ScanFunctionResult, + Schema, + SchemaInfo, + SchemaObjectType, + TableInfo, + TransactionOpaqueData, +) +from vgi.catalog.capabilities import ( + CONSTRAINT_ENFORCE, + CONSTRAINT_RECORDS_READ, + INDEX_DEFINITION_CREATE, + INDEX_DEFINITION_READ, + INDEX_DROP, + TRANSACTION_DDL, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import CatalogContentsResult, SchemaContentsInfo, SerializedSchema +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.indexes import IndexConstraintType, IndexDefinition +from vgi.catalog.options import OptionDefinitions, OptionState +from vgi.invocation import BindResponse, GlobalInitResponse +from vgi.table_function import BindParams, InitParams, ProcessParams, TableFunctionGenerator +from vgi.typed_values import TypedValueBatch +from vgi.worker import Worker + +ALGORITHM = "SQLITE.BTREE.V1" +TABLE = CatalogObjectRef(kind="table", schema_path=["main"], name="items") +COLUMN_FIELDS: list[pa.Field[Any]] = [pa.field("id", pa.int64()), pa.field("label", pa.string())] +COLUMNS = pa.schema(COLUMN_FIELDS) +PROFILES = CatalogCapabilities( + read=sorted([CONSTRAINT_RECORDS_READ, INDEX_DEFINITION_READ]), + ddl=sorted([INDEX_DEFINITION_CREATE, INDEX_DROP]), + write=[CONSTRAINT_ENFORCE], + transaction=[TRANSACTION_DDL], +) + + +def _quote(name: str) -> str: + return '"' + name.replace('"', '""') + '"' + + +class IndexAuthority: + """Store real indexes and their canonical records in the same transaction.""" + + def __init__(self, path: str) -> None: + self.path = path + with closing(sqlite3.connect(path)) as db, db: + db.execute("CREATE TABLE IF NOT EXISTS state (version INTEGER NOT NULL)") + if not db.execute("SELECT 1 FROM state").fetchone(): + db.execute("INSERT INTO state VALUES (0)") + db.execute("CREATE TABLE items (id INTEGER COLLATE BINARY, label TEXT COLLATE BINARY)") + db.executemany("INSERT INTO items VALUES (?, ?)", [(1, "one"), (2, "two")]) + db.execute("CREATE TABLE IF NOT EXISTS sessions (token BLOB PRIMARY KEY)") + db.execute( + "CREATE TABLE IF NOT EXISTS transactions " + "(token BLOB PRIMARY KEY, owner BLOB, status TEXT, base INTEGER, physical TEXT)" + ) + db.execute( + "CREATE TABLE IF NOT EXISTS index_records " + "(scope BLOB, name TEXT, physical TEXT UNIQUE, record BLOB, PRIMARY KEY(scope,name))" + ) + + def attach(self) -> bytes: + token = uuid.uuid4().bytes + with closing(sqlite3.connect(self.path)) as db, db: + db.execute("INSERT INTO sessions VALUES (?)", (token,)) + return token + + @contextmanager + def state(self, attach: bytes, tx: bytes | None = None) -> Iterator[tuple[sqlite3.Connection, str, bytes]]: + with closing(sqlite3.connect(self.path)) as db: + db.execute("BEGIN IMMEDIATE") + try: + if not db.execute("SELECT 1 FROM sessions WHERE token=?", (attach,)).fetchone(): + raise ValueError("unknown attachment") + physical, scope = "items", b"" + if tx is not None: + row = db.execute("SELECT owner,status,physical FROM transactions WHERE token=?", (tx,)).fetchone() + if row is None or row[0] != attach or row[1] != "active": + raise ValueError("unknown, foreign or terminal transaction") + physical, scope = row[2], tx + yield db, physical, scope + db.commit() + except BaseException: + db.rollback() + raise + + @staticmethod + def _validate(definition: IndexDefinition) -> list[str]: + definition.__post_init__() + if definition.table != TABLE or definition.algorithm != ALGORITHM: + raise ValueError("unsupported table or index algorithm") + if definition.associated_constraint is not None or definition.constraint_type == IndexConstraintType.PRIMARY: + raise ValueError("constraint-associated and PRIMARY indexes are unsupported") + if definition.options.schema.names or definition.options.schema.metadata: + raise ValueError("index algorithm accepts no options or option metadata") + keys = definition.key_definitions() + if any( + key.column_path is None or len(key.column_path) != 1 or key.column_path[0] not in COLUMNS.names + for key in keys + ): + raise ValueError("index algorithm requires top-level raw id/label keys with established BINARY collation") + columns = [key.column_path[0] for key in keys if key.column_path is not None] + if len(set(columns)) != len(columns): + raise ValueError("duplicate index keys are unsupported") + return columns + + @staticmethod + def _infos(db: sqlite3.Connection, scope: bytes) -> list[IndexInfo]: + infos = [] + for physical, encoded in db.execute( + "SELECT physical,record FROM index_records WHERE scope=? ORDER BY name", (scope,) + ): + info = IndexInfo.deserialize_from_bytes(encoded) + row = db.execute("SELECT tbl_name FROM sqlite_master WHERE type='index' AND name=?", (physical,)).fetchone() + if row is None: + raise ValueError("index metadata has no physical index") + keys = [item[2] for item in db.execute(f"PRAGMA index_info({_quote(physical)})")] + expected = IndexAuthority._validate(info.definition) + if keys != expected: + raise ValueError("index physical keys disagree with its definition") + listed = next(item for item in db.execute(f"PRAGMA index_list({_quote(row[0])})") if item[1] == physical) + if bool(listed[2]) != (info.definition.constraint_type == IndexConstraintType.UNIQUE): + raise ValueError("index physical uniqueness disagrees with its definition") + if any( + item[4] != "BINARY" or item[3] != 0 + for item in db.execute(f"PRAGMA index_xinfo({_quote(physical)})") + if item[5] + ): + raise ValueError("index physical collation/order disagrees with the qualified algorithm") + infos.append(info) + return infos + + @staticmethod + def _construct(db: sqlite3.Connection, table: str, scope: bytes, info: IndexInfo) -> None: + columns = IndexAuthority._validate(info.definition) + physical = "vgi_index_" + uuid.uuid4().hex + unique = "UNIQUE " if info.definition.constraint_type == IndexConstraintType.UNIQUE else "" + key_sql = ", ".join(_quote(name) + " COLLATE BINARY" for name in columns) + db.execute(f"CREATE {unique}INDEX {_quote(physical)} ON {_quote(table)} ({key_sql})") + db.execute( + "INSERT INTO index_records VALUES (?, ?, ?, ?)", (scope, info.name, physical, info.serialize_to_bytes()) + ) + + @staticmethod + def _remove(db: sqlite3.Connection, scope: bytes, name: str) -> bool: + row = db.execute("SELECT physical FROM index_records WHERE scope=? AND name=?", (scope, name)).fetchone() + if row is None: + return False + db.execute(f"DROP INDEX {_quote(row[0])}") + db.execute("DELETE FROM index_records WHERE scope=? AND name=?", (scope, name)) + return True + + def create( + self, attach: bytes, name: str, definition: IndexDefinition, conflict: OnConflict, tx: bytes | None + ) -> None: + self._validate(definition) + info = IndexInfo( + name=name, + schema_path=["main"], + comment=None, + tags={}, + definition=definition, + options=OptionState(values=definition.options, explicit_names=[]), + ) + with self.state(attach, tx) as (db, physical, scope): + exists = db.execute("SELECT 1 FROM index_records WHERE scope=? AND name=?", (scope, name)).fetchone() + if exists: + if conflict == OnConflict.IGNORE: + return + if conflict == OnConflict.ERROR: + raise ValueError("index already exists") + self._remove(db, scope, name) + self._construct(db, physical, scope, info) + if tx is None: + db.execute("UPDATE state SET version=version+1") + + def drop(self, attach: bytes, name: str, ignore: bool, tx: bytes | None) -> None: + with self.state(attach, tx) as (db, _, scope): + if not self._remove(db, scope, name): + if ignore: + return + raise ValueError("index does not exist") + if tx is None: + db.execute("UPDATE state SET version=version+1") + + def infos(self, attach: bytes, tx: bytes | None = None) -> tuple[int, list[IndexInfo]]: + with self.state(attach, tx) as (db, _, scope): + return db.execute("SELECT version FROM state").fetchone()[0], self._infos(db, scope) + + def rows(self, attach: bytes, tx: bytes | None = None) -> list[dict[str, Any]]: + with self.state(attach, tx) as (db, physical, _): + return [ + dict(zip(COLUMNS.names, row, strict=True)) + for row in db.execute(f"SELECT id,label FROM {_quote(physical)} ORDER BY rowid") + ] + + def replace_rows(self, attach: bytes, rows: list[dict[str, Any]]) -> None: + # Provider-side writes prove SQLite enforces every admitted UNIQUE index. + batch = pa.RecordBatch.from_pylist(rows, schema=COLUMNS) + with self.state(attach) as (db, physical, _): + db.execute(f"DELETE FROM {_quote(physical)}") + db.executemany( + f"INSERT INTO {_quote(physical)} VALUES (?, ?)", + [(row["id"], row["label"]) for row in batch.to_pylist()], + ) + db.execute("UPDATE state SET version=version+1") + + def begin(self, attach: bytes) -> bytes: + token = uuid.uuid4().bytes + physical = "vgi_tx_" + token.hex() + with self.state(attach) as (db, _, scope): + version = db.execute("SELECT version FROM state").fetchone()[0] + db.execute(f"CREATE TABLE {_quote(physical)} (id INTEGER COLLATE BINARY, label TEXT COLLATE BINARY)") + db.execute(f"INSERT INTO {_quote(physical)} SELECT * FROM items") + for info in self._infos(db, scope): + self._construct(db, physical, token, info) + db.execute("INSERT INTO transactions VALUES (?, ?, 'active', ?, ?)", (token, attach, version, physical)) + return token + + def finish(self, attach: bytes, token: bytes, commit: bool) -> None: + target = "committed" if commit else "rolled_back" + with self.state(attach) as (db, _, _): + row = db.execute("SELECT owner,status,base,physical FROM transactions WHERE token=?", (token,)).fetchone() + if row is None or row[0] != attach: + raise ValueError("unknown or foreign transaction") + if row[1] == target: + return + if row[1] != "active": + raise ValueError("terminal transaction") + if commit: + if row[2] != db.execute("SELECT version FROM state").fetchone()[0]: + raise ValueError("stale transaction revision") + pending = self._infos(db, token) + for info in self._infos(db, b""): + self._remove(db, b"", info.name) + for info in pending: + self._construct(db, "items", b"", info) + db.execute("UPDATE state SET version=version+1") + db.execute(f"DROP TABLE {_quote(row[3])}") + db.execute("DELETE FROM index_records WHERE scope=?", (token,)) + db.execute("UPDATE transactions SET status=? WHERE token=?", (target, token)) + + +@dataclass +class ScanState(ArrowSerializableDataclass): + """One bounded snapshot batch.""" + + emitted: bool = False + + +class IndexScan(TableFunctionGenerator[None, ScanState]): + """Scan actual SQLite rows; no public DML function is advertised.""" + + class Meta: + name = "index_reference_scan" + projection_pushdown = False + filter_pushdown = False + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + return BindResponse(output_schema=COLUMNS) + + @classmethod + def on_init(cls, params: InitParams[None]) -> GlobalInitResponse: + return GlobalInitResponse(max_workers=1) + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> ScanState: + return ScanState() + + @classmethod + def process(cls, params: ProcessParams[None], state: ScanState, out: OutputCollector) -> None: + if state.emitted: + out.finish() + return + if params.attach_opaque_data is None or params.init_call is None: + raise ValueError("reference scan requires an attachment") + tx = params.init_call.bind_call.transaction_opaque_data + rows = IndexProvider().authority.rows(params.attach_opaque_data, tx) + out.emit(pa.RecordBatch.from_pylist(rows, schema=params.output_schema)) + state.emitted = True + + +CATALOG = Catalog(name="indexes", default_schema="main", schemas=[Schema(path=["main"], functions=[IndexScan])]) + + +class IndexProvider(ReadOnlyCatalogInterface): + """Expose the finite authority through normal catalog and scan methods.""" + + catalog = CATALOG + database_path: ClassVar[str] + catalog_version_frozen = False + catalog_contents_attach_independent = False + + def __init__(self) -> None: + super().__init__() + self.authority = IndexAuthority(getattr(self, "database_path", "") or os.environ["VGI_INDEX_DATABASE"]) + + def catalog_capabilities(self, **kwargs: Any) -> CatalogCapabilities: + return PROFILES + + def catalog_attach(self, **kwargs: Any) -> CatalogAttachResult: + result = super().catalog_attach(**kwargs) + attach = AttachOpaqueData(self.authority.attach()) + return replace( + result, + attach_opaque_data=attach, + attach_opaque_data_required=True, + supports_transactions=True, + supports_catalog_contents=True, + catalog_version_frozen=False, + catalog_version=self.authority.infos(attach)[0], + ) + + def catalog_version( + self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes | None, **kwargs: Any + ) -> int: + return self.authority.infos(attach_opaque_data, transaction_opaque_data)[0] + + @staticmethod + def _schema(attach: bytes, index_count: int) -> SchemaInfo: + return SchemaInfo( + attach_opaque_data=AttachOpaqueData(attach), + path=["main"], + comment=None, + tags={}, + estimated_object_count={"table": 1, "table_function": 1, "index": index_count}, + ) + + def schemas(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes | None) -> list[SchemaInfo]: + _, infos = self.authority.infos(attach_opaque_data, transaction_opaque_data) + return [self._schema(attach_opaque_data, len(infos))] + + def schema_get(self, *, path: list[str], **kwargs: Any) -> SchemaInfo | None: + schemas = self.schemas(**kwargs) + return schemas[0] if path == ["main"] else None + + def table_get(self, *, schema_path: list[str], name: str, **kwargs: Any) -> TableInfo | None: + self.authority.infos(kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + if kwargs.get("at_unit") is not None or kwargs.get("at_value") is not None: + raise ValueError("reference does not support time travel") + if schema_path != ["main"] or name != "items": + return None + return TableInfo( + name=name, + schema_path=schema_path, + columns=SerializedSchema(COLUMNS.serialize().to_pybytes()), + constraints=[], + comment=None, + tags={}, + ) + + def schema_contents(self, **kwargs: Any) -> Any: + if kwargs["type"] == SchemaObjectType.INDEX: + return ( + self.authority.infos(kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"])[1] + if kwargs["path"] == ["main"] + else [] + ) + if kwargs["type"] == SchemaObjectType.TABLE: + info = self.table_get( + attach_opaque_data=kwargs["attach_opaque_data"], + transaction_opaque_data=kwargs["transaction_opaque_data"], + schema_path=kwargs["path"], + name="items", + ) + return [info] if info is not None else [] + return super().schema_contents(**kwargs) + + def catalog_contents(self, *, attach_opaque_data: bytes, if_none_match: str | None = None) -> CatalogContentsResult: + version, infos = self.authority.infos(attach_opaque_data) + table = self.table_get( + attach_opaque_data=attach_opaque_data, transaction_opaque_data=None, schema_path=["main"], name="items" + ) + assert table is not None + return CatalogContentsResult( + schemas=[ + SchemaContentsInfo( + schema=self._schema(attach_opaque_data, len(infos)), + tables=[table], + indexes=infos, + table_functions=ReadOnlyCatalogInterface.schema_contents( + self, + attach_opaque_data=AttachOpaqueData(attach_opaque_data), + transaction_opaque_data=None, + path=["main"], + type=SchemaObjectType.TABLE_FUNCTION, + ), + ) + ], + etag=str(version), + ) + + def table_scan_function_get(self, **kwargs: Any) -> ScanFunctionResult: + if self.table_get(**kwargs) is None: + raise ValueError("table does not exist") + return ScanFunctionResult(function_name="index_reference_scan", positional_arguments=[], named_arguments={}) + + def index_option_definitions( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + table: CatalogObjectRef, + algorithm: str, + ) -> OptionDefinitions: + self.authority.infos(attach_opaque_data, transaction_opaque_data) + if table != TABLE or algorithm != ALGORITHM: + raise ValueError("unsupported table or index algorithm") + return OptionDefinitions(schema=pa.schema([]), defaults=TypedValueBatch.empty()) + + def index_get( + self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes | None, schema_path: list[str], name: str + ) -> IndexInfo | None: + infos = self.authority.infos(attach_opaque_data, transaction_opaque_data)[1] + return next((info for info in infos if info.schema_path == schema_path and info.name == name), None) + + def index_create( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + schema_path: list[str], + name: str, + definition: IndexDefinition, + on_conflict: OnConflict, + ) -> None: + if schema_path != ["main"] or not name or "\0" in name: + raise ValueError("unsupported index namespace or name") + self.authority.create(attach_opaque_data, name, definition, on_conflict, transaction_opaque_data) + + def index_drop( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + schema_path: list[str], + name: str, + ignore_not_found: bool, + cascade: bool = False, + ) -> None: + if cascade or schema_path != ["main"]: + raise ValueError("reference does not support CASCADE or another schema") + self.authority.drop(attach_opaque_data, name, ignore_not_found, transaction_opaque_data) + + def catalog_transaction_begin(self, *, attach_opaque_data: bytes) -> TransactionOpaqueData: + return TransactionOpaqueData(self.authority.begin(attach_opaque_data)) + + def catalog_transaction_commit(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes) -> None: + self.authority.finish(attach_opaque_data, transaction_opaque_data, True) + + def catalog_transaction_rollback(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes) -> None: + self.authority.finish(attach_opaque_data, transaction_opaque_data, False) + + +class IndexWorker(Worker): + """Public typed index reference worker.""" + + catalog_interface = IndexProvider + catalog = CATALOG + + +if __name__ == "__main__": + IndexWorker.main() diff --git a/tests/catalog/query_native.py b/tests/catalog/query_native.py new file mode 100644 index 00000000..cc189308 --- /dev/null +++ b/tests/catalog/query_native.py @@ -0,0 +1,180 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D107 +"""Durable query fixture and audit trail for the native DuckDB 1.5 adapter. + +Only the finite queries from query_reference are admitted. The extra SQLite +events let the native runner distinguish binding from execution, and inspect +parameter types, transaction owners and cleanup after the worker has exited. +""" + +from __future__ import annotations + +import json +import os +import sqlite3 +import time +from contextlib import closing +from pathlib import Path +from typing import Any + +import pyarrow as pa +from vgi_rpc.rpc import CallContext + +from vgi.catalog import AttachOpaqueData, CatalogAttachResult, TransactionOpaqueData +from vgi.catalog.query import ( + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, +) +from vgi.protocol import ClientCapabilities +from vgi.query_parameters import QueryParameters +from vgi.query_provider import QueryExecution, QueryOwner, QueryPreparation +from vgi.worker import Worker + +from .query_reference import QueryReferenceCatalog, QueryReferenceProvider + + +class NativeQueryProvider(QueryReferenceProvider): + """Add durable observations and explicitly selected failure modes.""" + + def __init__(self, catalog: NativeQueryCatalog) -> None: + self.catalog: NativeQueryCatalog = catalog + super().__init__(catalog) + self.mode = os.environ.get("VGI_QUERY_NATIVE_MODE", "") + self.enabled = self.mode != "disabled" + + def audit( + self, event: str, owner: QueryOwner, *, handle: bytes | None = None, detail: dict[str, object] | None = None + ) -> None: + self.catalog.audit(event, owner.attach_opaque_data, owner.transaction_opaque_data, handle, detail) + + def capabilities(self, *, owner: QueryOwner, offer: ClientQueryCapabilities) -> QueryCapabilities | None: + self.audit("capabilities", owner) + return super().capabilities(owner=owner, offer=offer) + + def prepare(self, *, owner: QueryOwner, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + self.audit("prepare_attempt", owner, detail={"query": request.query}) + if self.mode == "prepare_error": + raise ValueError("native fixture preparation failure") + result = super().prepare(owner=owner, request=request) + self.audit("prepare", owner, handle=result.prepared_query_token, detail={"status": result.status}) + if self.mode == "prepare_disconnect": + # A preparation already exists, but its response never reaches the + # client. Retrying would allocate a second resource invisibly. + os._exit(71) + return result + + def start( + self, *, owner: QueryOwner, preparation: QueryPreparation, parameters: QueryParameters, execution_id: bytes + ) -> QueryExecution: + self.audit( + "start_attempt", + owner, + handle=preparation.result.prepared_query_token, + detail={ + "query": preparation.request.query, + "names": parameters.schema.names, + "types": [str(field.type) for field in parameters.schema], + "values": [parameters.batch.column(i)[0].as_py() for i in range(parameters.batch.num_columns)], + "execution_id": execution_id.hex(), + }, + ) + execution = super().start( + owner=owner, preparation=preparation, parameters=parameters, execution_id=execution_id + ) + self.audit("start", owner, handle=execution.handle) + if self.mode == "start_error": + # SQL already ran. A lost/error init response must never cause the + # native client to invoke start again; transaction end reclaims it. + raise OSError("native fixture ambiguous start failure") + return execution + + def read(self, *, owner: QueryOwner, handle: bytes, position: int) -> pa.RecordBatch | None: + self.audit("read", owner, handle=handle, detail={"position": position}) + if self.mode == "read_error": + raise RuntimeError("native fixture read failure") + if self.mode == "read_wrong_type": + return pa.record_batch([pa.array(["wrong result type"])], names=["result"]) + if self.mode == "slow_read" and position == 0: + # The runner waits for the event, then interrupts the native query. + # Keep this bounded even if cancellation never reaches the worker. + if marker := os.environ.get("VGI_QUERY_READ_MARKER"): + Path(marker).touch() + time.sleep(3) + return super().read(owner=owner, handle=handle, position=position) + + def close(self, *, owner: QueryOwner, handle: bytes, reason: str) -> None: + super().close(owner=owner, handle=handle, reason=reason) + self.audit("close", owner, handle=handle, detail={"reason": reason}) + + def release(self, *, owner: QueryOwner, recipe: bytes) -> None: + super().release(owner=owner, recipe=recipe) + self.audit("release", owner, handle=recipe) + + +class NativeQueryCatalog(QueryReferenceCatalog): + """Retain the finite provider's durable attachment and transaction rules.""" + + def __init__(self) -> None: + super().__init__() + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute( + "CREATE TABLE IF NOT EXISTS native_query_events " + "(id INTEGER PRIMARY KEY, event TEXT, attach BLOB, tx BLOB, handle BLOB, detail TEXT)" + ) + self.query_provider = NativeQueryProvider(self) + + def audit( + self, + event: str, + attach: bytes, + tx: bytes | None = None, + handle: bytes | None = None, + detail: dict[str, object] | None = None, + ) -> None: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute( + "INSERT INTO native_query_events(event, attach, tx, handle, detail) VALUES (?, ?, ?, ?, ?)", + (event, attach, tx, handle, json.dumps(detail or {})), + ) + + def catalog_attach( + self, + *, + name: str, + options: dict[str, Any], + data_version_spec: str | None, + implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, + ctx: CallContext | None = None, + ) -> CatalogAttachResult: + result = super().catalog_attach( + name=name, + options=options, + data_version_spec=data_version_spec, + implementation_version=implementation_version, + client_capabilities=client_capabilities, + ctx=ctx, + ) + self.audit("attach", result.attach_opaque_data) + return result + + def catalog_detach(self, *, attach_opaque_data: AttachOpaqueData) -> None: + super().catalog_detach(attach_opaque_data=attach_opaque_data) + self.audit("detach", attach_opaque_data) + + def _finish(self, attach: AttachOpaqueData, tx: TransactionOpaqueData, status: str) -> None: + super()._finish(attach, tx, status) + self.audit("transaction_finish", attach, tx, detail={"status": status}) + + +class NativeQueryWorker(Worker): + """Expose the audited fixture through the normal subprocess transport.""" + + catalog_interface = NativeQueryCatalog + + +if __name__ == "__main__": + NativeQueryCatalog.database_path = os.environ["VGI_QUERY_REFERENCE_DB"] + NativeQueryWorker.main() diff --git a/tests/catalog/query_reference.py b/tests/catalog/query_reference.py new file mode 100644 index 00000000..cc5db8f4 --- /dev/null +++ b/tests/catalog/query_reference.py @@ -0,0 +1,358 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103, D107 +"""Finite read-query reference with portable SQLite preparations and result fragments. + +This is a lifecycle fixture, not a general SQL sandbox. Only the exact queries +below are supported. DuckDB binds typed NULLs for metadata during preparation; +only start fetches result rows. Results are materialized before returning init, +so another process can continue without serializing a cursor or rerunning SQL. +The storage/latency cost is intentional and is not a production performance claim. +""" + +from __future__ import annotations + +import sqlite3 +import time +import uuid +from contextlib import closing +from dataclasses import replace +from typing import Any, ClassVar + +import duckdb +import pyarrow as pa +from vgi_rpc.rpc import CallContext +from vgi_rpc.utils import deserialize_record_batch, serialize_record_batch_bytes + +from vgi.catalog import AttachOpaqueData, CatalogAttachResult, TransactionOpaqueData +from vgi.catalog.query import ( + QUERY_ENCODING, + QUERY_EXPLICIT, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, + QueryEvaluationContext, + read_query_schema, +) +from vgi.protocol import ClientCapabilities +from vgi.query_parameters import PARAMETER_MODE_KEY, QueryParameters, read_parameter_schema +from vgi.query_provider import CatalogQueryProvider, QueryExecution, QueryOwner, QueryPreparation +from vgi.worker import Worker + +from .schema_options_reference import SchemaOptionsCatalog + +QUERIES = { + "SELECT $value AS result": ["value"], + "SELECT $value + $value AS result": ["value"], + "SELECT $1 AS result": ["1"], + "SELECT i * $factor AS result FROM numbers ORDER BY i": ["factor"], + "SELECT i AS result FROM numbers WHERE false": [], + "SELECT i AS result FROM numbers ORDER BY i": [], +} +TYPES = {pa.int32(): duckdb.sqltypes.INTEGER, pa.int64(): duckdb.sqltypes.BIGINT, pa.string(): duckdb.sqltypes.VARCHAR} +ARROW_TYPES = {str(v): k for k, v in TYPES.items()} + + +def now_ms() -> int: + return time.time_ns() // 1_000_000 + + +class QueryReferenceProvider(CatalogQueryProvider): + """Bind a finite query set and store ordered results with bounded leases.""" + + revision = "finite-reference-1" + enabled = True + lifetime_ms = 120_000 + max_resources = 64 + max_bytes = 16 * 1024 * 1024 + + def __init__(self, catalog: QueryReferenceCatalog) -> None: + self.catalog = catalog + + def _db(self) -> sqlite3.Connection: + return sqlite3.connect(self.catalog.database_path) + + def _authorize(self, db: sqlite3.Connection, owner: QueryOwner) -> int: + if not db.execute("SELECT 1 FROM query_attachments WHERE id=?", (owner.attach_opaque_data,)).fetchone(): + raise PermissionError("query attachment is closed") + return self.catalog._snapshot( + db, + AttachOpaqueData(owner.attach_opaque_data), + None if owner.transaction_opaque_data is None else TransactionOpaqueData(owner.transaction_opaque_data), + )[0] + + @staticmethod + def _sweep(db: sqlite3.Connection) -> None: + db.execute("DELETE FROM query_preparations WHERE expires<=?", (now_ms(),)) + db.execute("DELETE FROM query_executions WHERE expires<=?", (now_ms(),)) + db.execute("DELETE FROM query_fragments WHERE handle NOT IN (SELECT handle FROM query_executions)") + + def sweep(self) -> None: + with closing(self._db()) as db, db: + self._sweep(db) + + def _quota(self, db: sqlite3.Connection) -> None: + count = sum( + db.execute(f"SELECT count(*) FROM {table}").fetchone()[0] + for table in ["query_preparations", "query_executions"] + ) + size = db.execute("SELECT coalesce(sum(length(payload)), 0) FROM query_fragments").fetchone()[0] + if count >= self.max_resources or size >= self.max_bytes: + raise RuntimeError("query resource quota exceeded") + + def capabilities(self, *, owner: QueryOwner, offer: ClientQueryCapabilities) -> QueryCapabilities | None: + with closing(self._db()) as db: + self._authorize(db, owner) + if not self.enabled: + return None + return QueryCapabilities( + encoding=QUERY_ENCODING, + semantic_profiles=[QUERY_EXPLICIT], + execution_modes=["explicit"], + backend_engine="duckdb", + backend_version=duckdb.__version__, + capability_revision=self.revision, + default_evaluation_context=QueryEvaluationContext(), + ) + + @staticmethod + def _connection() -> duckdb.DuckDBPyConnection: + con = duckdb.connect(config={"enable_external_access": False, "threads": 1}) + con.register("numbers", pa.table({"i": pa.array(range(7), type=pa.int64())})) + return con + + @staticmethod + def _parameters(schema: pa.Schema, values: QueryParameters | None = None) -> dict[str, duckdb.Value]: + return { + field.name: duckdb.Value(None if values is None else values.batch.column(i)[0].as_py(), TYPES[field.type]) + for i, field in enumerate(schema) + } + + @staticmethod + def _sql(query: str, schema: pa.Schema) -> str: + assert query in QUERIES + for field in schema: + query = query.replace(f"${field.name}", f"CAST(${field.name} AS {TYPES[field.type]})") + return query + + @staticmethod + def _output(relation: duckdb.DuckDBPyRelation) -> pa.Schema: + return pa.schema( + [ + pa.field(name, ARROW_TYPES[str(dtype)]) + for name, dtype in zip(relation.columns, relation.types, strict=True) + ] + ) + + def prepare(self, *, owner: QueryOwner, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + with closing(self._db()) as db, db: + db.execute("BEGIN IMMEDIATE") + version = self._authorize(db, owner) + self._sweep(db) + self._quota(db) + if request.query not in QUERIES or request.mode != "explicit": + return CatalogQueryPrepareResult(status="declined", reason_code="unsupported_query") + schema = read_parameter_schema(request.parameter_schema) + if schema.names != QUERIES[request.query]: + raise ValueError("query placeholder mapping does not match parameter schema") + if any(f.type not in TYPES or f.metadata for f in schema) or set(schema.metadata or {}) != { + PARAMETER_MODE_KEY + }: + return CatalogQueryPrepareResult(status="declined", reason_code="unsupported_parameter_type") + with self._connection() as con: + relation = con.sql(self._sql(request.query, schema), params=self._parameters(schema)) + assert relation is not None + output = self._output(relation) # No fetch/execute or result data is touched. + result = CatalogQueryPrepareResult( + status="accepted", + prepared_query_token=uuid.uuid4().bytes, + output_schema=output.serialize().to_pybytes(), + result_ordering="query_order", + validated_catalog_version=version, + context_digest=request.evaluation_context.digest(request.encoding, request.semantic_profile), + expires_at_epoch_ms=now_ms() + self.lifetime_ms, + ) + result.validate_request(request) + db.execute( + "INSERT INTO query_preparations VALUES (?, ?, ?, ?, ?, ?, ?)", + ( + result.prepared_query_token, + owner.scope, + owner.attach_opaque_data, + owner.transaction_opaque_data, + result.expires_at_epoch_ms, + request.serialize_to_bytes(), + result.serialize_to_bytes(), + ), + ) + return result + + def _resolve(self, db: sqlite3.Connection, owner: QueryOwner, recipe: bytes) -> QueryPreparation: + self._authorize(db, owner) + row = db.execute( + "SELECT request, result, expires FROM query_preparations WHERE handle=? AND scope=?", (recipe, owner.scope) + ).fetchone() + if row is None or row[2] <= now_ms(): + raise ValueError("query preparation is missing or expired") + return QueryPreparation( + request=CatalogQueryPrepareRequest.deserialize_from_bytes(row[0]), + result=CatalogQueryPrepareResult.deserialize_from_bytes(row[1]), + ) + + def resolve(self, *, owner: QueryOwner, recipe: bytes) -> QueryPreparation: + with closing(self._db()) as db: + return self._resolve(db, owner, recipe) + + def start( + self, *, owner: QueryOwner, preparation: QueryPreparation, parameters: QueryParameters, execution_id: bytes + ) -> QueryExecution: + with closing(self._db()) as db, db: + db.execute("BEGIN IMMEDIATE") + self._sweep(db) + assert preparation.result.prepared_query_token is not None + saved = self._resolve(db, owner, preparation.result.prepared_query_token) + if saved != preparation: + raise ValueError("query preparation changed") + self._quota(db) + parameters.validate_prepared_schema(saved.request.parameter_schema) + with self._connection() as con: + relation = con.sql( + self._sql(saved.request.query, parameters.schema), + params=self._parameters(parameters.schema, parameters), + ) + assert relation is not None + assert saved.result.output_schema is not None + if not self._output(relation).equals( + read_query_schema(saved.result.output_schema), check_metadata=True + ): + raise ValueError("query output changed") + batches = list(relation.to_arrow_reader(batch_size=2)) + fragments = [serialize_record_batch_bytes(batch) for batch in batches] + existing = db.execute("SELECT coalesce(sum(length(payload)),0) FROM query_fragments").fetchone()[0] + if existing + sum(map(len, fragments)) > self.max_bytes: + raise RuntimeError("query result quota exceeded") + handle = uuid.uuid4().bytes + expires = now_ms() + self.lifetime_ms + db.execute( + "INSERT INTO query_executions VALUES (?, ?, ?, ?, ?, ?)", + ( + handle, + owner.scope, + owner.attach_opaque_data, + owner.transaction_opaque_data, + expires, + len(fragments), + ), + ) + db.executemany( + "INSERT INTO query_fragments VALUES (?, ?, ?)", + [(handle, i, payload) for i, payload in enumerate(fragments)], + ) + db.execute("UPDATE query_audit SET starts=starts+1") + return QueryExecution(handle=handle, expires_at_epoch_ms=expires) + + def read(self, *, owner: QueryOwner, handle: bytes, position: int) -> pa.RecordBatch | None: + with closing(self._db()) as db: + self._authorize(db, owner) + row = db.execute( + "SELECT expires, batches FROM query_executions WHERE handle=? AND scope=?", (handle, owner.scope) + ).fetchone() + if row is None or row[0] <= now_ms(): + raise ValueError("query execution is missing or expired; it cannot be restarted") + if not 0 <= position <= row[1]: + raise ValueError("query position out of bounds") + if position == row[1]: + return None + payload = db.execute( + "SELECT payload FROM query_fragments WHERE handle=? AND position=?", (handle, position) + ).fetchone() + if payload is None: + raise ValueError("query execution lost a result fragment") + return deserialize_record_batch(payload[0])[0] + + def close(self, *, owner: QueryOwner, handle: bytes, reason: str) -> None: + with closing(self._db()) as db, db: + db.execute("DELETE FROM query_executions WHERE handle=? AND scope=?", (handle, owner.scope)) + self._sweep(db) + + def release(self, *, owner: QueryOwner, recipe: bytes) -> None: + with closing(self._db()) as db, db: + db.execute("DELETE FROM query_preparations WHERE handle=? AND scope=?", (recipe, owner.scope)) + self._sweep(db) + + +class QueryReferenceCatalog(SchemaOptionsCatalog): + """Give query resources durable attachment and transaction lifetimes.""" + + catalog_name = "queries" + database_path: ClassVar[str] + # Schema transactions supply durable transaction identity; the query fixture + # reads an immutable seven-row relation and claims no DDL or DML integration. + + def __init__(self) -> None: + super().__init__() + self.query_provider = QueryReferenceProvider(self) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("CREATE TABLE IF NOT EXISTS query_attachments (id BLOB PRIMARY KEY)") + db.execute( + "CREATE TABLE IF NOT EXISTS query_preparations " + "(handle BLOB PRIMARY KEY, scope BLOB, attach BLOB, tx BLOB, " + "expires INTEGER, request BLOB, result BLOB)" + ) + db.execute( + "CREATE TABLE IF NOT EXISTS query_executions " + "(handle BLOB PRIMARY KEY, scope BLOB, attach BLOB, tx BLOB, expires INTEGER, batches INTEGER)" + ) + db.execute( + "CREATE TABLE IF NOT EXISTS query_fragments " + "(handle BLOB, position INTEGER, payload BLOB, PRIMARY KEY(handle, position))" + ) + db.execute("CREATE TABLE IF NOT EXISTS query_audit (id INTEGER PRIMARY KEY, starts INTEGER)") + db.execute("INSERT OR IGNORE INTO query_audit VALUES (1, 0)") + + def catalog_attach( + self, + *, + name: str, + options: dict[str, Any], + data_version_spec: str | None, + implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, + ctx: CallContext | None = None, + ) -> CatalogAttachResult: + result = super().catalog_attach( + name=name, + options=options, + data_version_spec=data_version_spec, + implementation_version=implementation_version, + client_capabilities=client_capabilities, + ctx=ctx, + ) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("INSERT INTO query_attachments VALUES (?)", (result.attach_opaque_data,)) + return replace(result, attach_opaque_data_required=True) + + def catalog_detach(self, *, attach_opaque_data: AttachOpaqueData) -> None: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("DELETE FROM query_attachments WHERE id=?", (attach_opaque_data,)) + for table in ["query_preparations", "query_executions"]: + db.execute(f"DELETE FROM {table} WHERE attach=?", (attach_opaque_data,)) + QueryReferenceProvider._sweep(db) + + def _finish(self, attach: AttachOpaqueData, tx: TransactionOpaqueData, status: str) -> None: + super()._finish(attach, tx, status) + with closing(sqlite3.connect(self.database_path)) as db, db: + for table in ["query_preparations", "query_executions"]: + db.execute(f"DELETE FROM {table} WHERE attach=? AND tx=?", (attach, tx)) + QueryReferenceProvider._sweep(db) + + +class QueryReferenceWorker(Worker): + """Serve the query fixture through normal VGI dispatch.""" + + catalog_interface = QueryReferenceCatalog + + +OFFER = ClientQueryCapabilities( + encodings=[QUERY_ENCODING], semantic_profiles=[QUERY_EXPLICIT], engine_version="fixture" +) diff --git a/tests/catalog/required_filters_native.py b/tests/catalog/required_filters_native.py new file mode 100644 index 00000000..4ce1d20e --- /dev/null +++ b/tests/catalog/required_filters_native.py @@ -0,0 +1,153 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D106 +"""Native path-identity and malformed-metadata fixture; never a production provider.""" + +from __future__ import annotations + +import os +from dataclasses import fields, replace +from typing import Any + +import pyarrow as pa + +from vgi._test_fixtures.table.catalog_scans import _static_scan_function +from vgi.catalog import Catalog, ReadOnlyCatalogInterface, Schema, Table +from vgi.protocol import CatalogContentsResponse, TablesResponse +from vgi.worker import Worker + +COLUMNS = pa.schema( + [ + ("a.b", pa.int64()), + ( + "a", + pa.struct( + [ + ("b", pa.int64()), + ("MixedCase", pa.int64()), + ('q."uote', pa.int64()), + ("deep", pa.struct([("leaf", pa.int64()), ("other", pa.int64())])), + ] + ), + ), + ] +) +Scan = _static_scan_function( + "required_path_scan", + "Literal and nested identifiers", + COLUMNS, + {"a.b": [1], "a": [{"b": 1, "MixedCase": 1, 'q."uote': 1, "deep": {"leaf": 1, "other": 1}}]}, +) +CATALOG = Catalog( + name="paths", + default_schema="main", + schemas=[ + Schema( + path=["main"], + tables=[ + Table(name="literal", function=Scan, required_filters=((("a.b",),),)), + Table(name="nested", function=Scan, required_filters=((("a", "b"),),)), + Table(name="case_path", function=Scan, required_filters=((("a", "MixedCase"),),)), + Table(name="both_paths", function=Scan, required_filters=((("a.b",),), (("a", "b"),))), + Table(name="parent_path", function=Scan, required_filters=((("a",),),)), + Table(name="deep_path", function=Scan, required_filters=((("a", "deep", "leaf"),),)), + Table(name="quoted_path", function=Scan, required_filters=((("a", 'q."uote'),),)), + ], + ) + ], +) + + +def corrupt_wire(raw: bytes | None) -> bytes | None: + """Corrupt encoded item/collection streams after normal provider validation.""" + mode = os.environ.get("VGI_REQUIRED_FILTER_MODE") + if raw is None or not mode: + return raw + values: dict[str, Any] = { + "null": None, + "null_group": [None], + "empty_group": [[]], + "null_path": [[None]], + "empty_path": [[[]]], + "null_component": [[[None]]], + "empty_component": [[[""]]], + "nul_component": [[["a\0"]]], + "missing_component": [[["a", "typo"]]], + "scalar_parent": [[["a.b", "child"]]], + "duplicate_path": [[["a.b"], ["a.b"]]], + "legacy": [["a.b"]], + } + batches = [] + with pa.ipc.open_stream(raw) as reader: + for batch in reader: + index = batch.schema.get_field_index("required_filters") + if mode == "missing": + batch = batch.remove_column(index) + else: + shape = ( + pa.list_(pa.list_(pa.string())) if mode == "legacy" else pa.list_(pa.list_(pa.list_(pa.string()))) + ) + batch = batch.set_column( + index, + pa.field("required_filters", shape, nullable=False), + pa.array([values[mode]] * batch.num_rows, type=shape), + ) + batches.append(batch) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, batches[0].schema) as writer: + for batch in batches: + writer.write_batch(batch) + return sink.getvalue().to_pybytes() + + +class PathCatalog(ReadOnlyCatalogInterface): + """Expose valid metadata; corruption happens only at the test wire boundary.""" + + catalog = CATALOG + + def catalog_attach(self, **kwargs: Any) -> Any: + return replace(super().catalog_attach(**kwargs), supports_catalog_contents=False) + + +class PathWorker(Worker): + """Serve this fixture over the ordinary worker transport.""" + + catalog = CATALOG + catalog_interface = PathCatalog + + def catalog_table_get( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + at_unit: str | None = None, + at_value: str | None = None, + transaction_opaque_data: bytes | None = None, + ) -> TablesResponse: + result = super().catalog_table_get( + attach_opaque_data, schema_path, name, at_unit, at_value, transaction_opaque_data + ) + return replace(result, items=[corrupt_wire(item) for item in result.items]) + + def catalog_schema_contents_tables( + self, + attach_opaque_data: bytes, + path: list[str], + transaction_opaque_data: bytes | None = None, + ) -> TablesResponse: + result = super().catalog_schema_contents_tables(attach_opaque_data, path, transaction_opaque_data) + return replace(result, items=[corrupt_wire(item) for item in result.items]) + + def catalog_contents( + self, + attach_opaque_data: bytes, + if_none_match: str | None = None, + ) -> CatalogContentsResponse: + result = super().catalog_contents(attach_opaque_data, if_none_match) + return CatalogContentsResponse( + **{field.name: getattr(result, field.name) for field in fields(CatalogContentsResponse)} + | {"tables": corrupt_wire(result.tables)} + ) + + +if __name__ == "__main__": + PathWorker.main() diff --git a/tests/catalog/schema_options_reference.py b/tests/catalog/schema_options_reference.py new file mode 100644 index 00000000..165d44a9 --- /dev/null +++ b/tests/catalog/schema_options_reference.py @@ -0,0 +1,370 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D107 + +"""Durable schema-option reference backend used by public-RPC lifecycle tests. + +SQLite stores both committed metadata and transaction snapshots. No Python +process-local map is needed for reconnects or transaction completion. This +reference is deliberately limited to schemas; it advertises no mixed DDL/DML, +write replay, or distributed atomicity guarantee. +""" + +from __future__ import annotations + +import base64 +import json +import sqlite3 +import uuid +from contextlib import closing +from dataclasses import replace +from typing import Any, ClassVar + +import pyarrow as pa +from vgi_rpc.rpc import CallContext + +from vgi.catalog import ( + AttachOpaqueData, + CatalogAttachResult, + ReadOnlyCatalogInterface, + SchemaInfo, + TransactionOpaqueData, +) +from vgi.catalog.capabilities import ( + SCHEMA_METADATA_ALTER, + SCHEMA_OPTIONS_ALTER, + SCHEMA_OPTIONS_CREATE, + SCHEMA_OPTIONS_READ, + TRANSACTION_DDL, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import CatalogContentsResult, OnConflict, SchemaContentsInfo +from vgi.catalog.metadata_changes import CatalogMetadataChanges +from vgi.catalog.options import OptionChanges, OptionDefinitions +from vgi.protocol import ClientCapabilities +from vgi.schema_path import schema_path_key +from vgi.typed_values import TypedValueBatch +from vgi.worker import Worker + +SCHEMA_FIELDS: list[pa.Field[Any]] = [ + pa.field("label", pa.string(), metadata={b"purpose": b"display"}), + pa.field("limit", pa.int32(), nullable=False), + pa.field("locked", pa.bool_(), nullable=False), +] +SCHEMA = pa.schema(SCHEMA_FIELDS, metadata={b"provider": b"schema-options-reference/v1"}) +DEFINITIONS = OptionDefinitions( + schema=SCHEMA, + defaults=TypedValueBatch( + pa.RecordBatch.from_pylist([{"label": "default", "limit": 10, "locked": True}], schema=SCHEMA) + ), + create_names=["label", "limit"], + alter_names=["label", "limit"], +) +PROFILES = CatalogCapabilities( + read=[SCHEMA_OPTIONS_READ], + ddl=sorted([SCHEMA_OPTIONS_CREATE, SCHEMA_OPTIONS_ALTER, SCHEMA_METADATA_ALTER]), + transaction=[TRANSACTION_DDL], +) + + +def values(**kwargs: Any) -> TypedValueBatch: + """Build a typed subset with the provider's full field/schema metadata.""" + schema = pa.schema([SCHEMA.field(name) for name in kwargs]).with_metadata(SCHEMA.metadata) + return TypedValueBatch(pa.RecordBatch.from_pylist([kwargs], schema=schema)) + + +def _key(path: list[str]) -> str: + return json.dumps(schema_path_key(path)) + + +class SchemaOptionsCatalog(ReadOnlyCatalogInterface): + """Serializable snapshot transactions, atomic changes and persistent readback.""" + + catalog_name = "options" + database_path: ClassVar[str] + profiles: ClassVar[CatalogCapabilities] = PROFILES + catalog_version_frozen = False + catalog_contents_attach_independent = True # Exercise the worker's capability cache guard. + + def __init__(self) -> None: + super().__init__() + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("CREATE TABLE IF NOT EXISTS catalog (id INTEGER PRIMARY KEY, version INTEGER, data TEXT)") + db.execute( + "CREATE TABLE IF NOT EXISTS transactions " + "(id BLOB PRIMARY KEY, owner BLOB, base INTEGER, version INTEGER, data TEXT, status TEXT)" + ) + initial = SchemaInfo( + attach_opaque_data=AttachOpaqueData(b""), + path=["main"], + comment=None, + tags={}, + options=DEFINITIONS.create(), + ) + db.execute( + "INSERT OR IGNORE INTO catalog VALUES (1, 1, ?)", (json.dumps({_key(["main"]): self._encode(initial)}),) + ) + + @staticmethod + def _encode(info: SchemaInfo) -> str: + return base64.b64encode(info.serialize_to_bytes()).decode() + + @staticmethod + def _decode(value: str, attach: AttachOpaqueData) -> SchemaInfo: + return replace(SchemaInfo.deserialize_from_bytes(base64.b64decode(value)), attach_opaque_data=attach) + + def catalog_attach( + self, + *, + name: str, + options: dict[str, Any], + data_version_spec: str | None, + implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, + ctx: CallContext | None = None, + ) -> CatalogAttachResult: + result = super().catalog_attach( + name=name, + options=options, + data_version_spec=data_version_spec, + implementation_version=implementation_version, + client_capabilities=client_capabilities, + ctx=ctx, + ) + attach = AttachOpaqueData(uuid.uuid4().bytes) + return replace( + result, + attach_opaque_data=attach, + attach_opaque_data_required=True, + supports_transactions=True, + catalog_version_frozen=False, + catalog_version=self.catalog_version(attach_opaque_data=attach, transaction_opaque_data=None), + ) + + def catalog_capabilities(self, *, attach_opaque_data: AttachOpaqueData) -> CatalogCapabilities: + return self.profiles + + @staticmethod + def _snapshot( + db: sqlite3.Connection, attach: AttachOpaqueData, tx: TransactionOpaqueData | None + ) -> tuple[int, dict[str, str]]: + if tx is None: + version, data = db.execute("SELECT version, data FROM catalog WHERE id=1").fetchone() + else: + row = db.execute( + "SELECT version, data, status FROM transactions WHERE id=? AND owner=?", (tx, attach) + ).fetchone() + if row is None or row[2] != "open": + raise ValueError("transaction is not open") + version, data, _ = row + return version, json.loads(data) + + def catalog_version( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + ctx: Any = None, + ) -> int: + with closing(sqlite3.connect(self.database_path)) as db: + return self._snapshot(db, attach_opaque_data, transaction_opaque_data)[0] + + def schemas( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None + ) -> list[SchemaInfo]: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + return [self._decode(value, attach_opaque_data) for value in data.values()] + + def schema_get( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + ) -> SchemaInfo | None: + return next( + ( + info + for info in self.schemas( + attach_opaque_data=attach_opaque_data, transaction_opaque_data=transaction_opaque_data + ) + if _key(info.path) == _key(path) + ), + None, + ) + + def catalog_contents( + self, *, attach_opaque_data: AttachOpaqueData, if_none_match: str | None = None + ) -> CatalogContentsResult: + return CatalogContentsResult( + schemas=[ + SchemaContentsInfo(schema=info) + for info in self.schemas(attach_opaque_data=attach_opaque_data, transaction_opaque_data=None) + ], + etag="deliberately-constant-provider-etag", + ) + + def schema_option_definitions( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + ) -> OptionDefinitions: + schema_path_key(path) + return DEFINITIONS + + def _mutate( + self, + attach: AttachOpaqueData, + tx: TransactionOpaqueData | None, + path: list[str], + *, + create: bool, + changes: OptionChanges | None = None, + options: TypedValueBatch | None = None, + on_conflict: OnConflict = OnConflict.ERROR, + comment: str | None = None, + tags: dict[str, str] | None = None, + ) -> None: + key = _key(path) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, tx) + if create: + candidate = DEFINITIONS.create(options) + if len(path) > 1 and _key(path[:-1]) not in data: + raise ValueError("parent schema does not exist") + if key in data: + if on_conflict == OnConflict.IGNORE: + return + if on_conflict != OnConflict.REPLACE: + raise ValueError("schema already exists") + info = SchemaInfo( + attach_opaque_data=AttachOpaqueData(b""), + path=path, + comment=comment, + tags=tags or {}, + options=candidate, + ) + else: + if key not in data: + raise ValueError("schema does not exist") + info = self._decode(data[key], AttachOpaqueData(b"")) + assert info.options is not None and changes is not None + info = replace(info, options=DEFINITIONS.apply(info.options, changes)) + data[key] = self._encode(info) + if tx is None: + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version + 1, json.dumps(data))) + else: + db.execute("UPDATE transactions SET version=?, data=? WHERE id=?", (version + 1, json.dumps(data), tx)) + + def schema_create( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + on_conflict: OnConflict = OnConflict.ERROR, + comment: str | None, + tags: dict[str, str], + options: TypedValueBatch | None = None, + ) -> None: + self._mutate( + attach_opaque_data, + transaction_opaque_data, + path, + create=True, + options=options, + on_conflict=on_conflict, + comment=comment, + tags=tags, + ) + + def schema_alter_metadata( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + changes: CatalogMetadataChanges, + ignore_not_found: bool = False, + ) -> None: + changes.__post_init__() + key = _key(path) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + if key not in data: + if ignore_not_found: + return + raise ValueError("schema does not exist") + previous = self._decode(data[key], AttachOpaqueData(b"")) + comment, tags = changes.apply_to(previous.comment, previous.tags) + if comment == previous.comment and tags == previous.tags: + return + data[key] = self._encode(replace(previous, comment=comment, tags=tags)) + if transaction_opaque_data is None: + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version + 1, json.dumps(data))) + else: + db.execute( + "UPDATE transactions SET version=?, data=? WHERE id=?", + (version + 1, json.dumps(data), transaction_opaque_data), + ) + + def schema_alter_options( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + changes: OptionChanges, + ) -> None: + self._mutate(attach_opaque_data, transaction_opaque_data, path, create=False, changes=changes) + + def catalog_transaction_begin(self, *, attach_opaque_data: AttachOpaqueData) -> TransactionOpaqueData: + tx = TransactionOpaqueData(uuid.uuid4().bytes) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach_opaque_data, None) + db.execute( + "INSERT INTO transactions VALUES (?, ?, ?, ?, ?, 'open')", + (tx, attach_opaque_data, version, version, json.dumps(data)), + ) + return tx + + def _finish(self, attach: AttachOpaqueData, tx: TransactionOpaqueData, status: str) -> None: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + row = db.execute( + "SELECT base, version, data, status FROM transactions WHERE id=? AND owner=?", (tx, attach) + ).fetchone() + if row is None: + raise ValueError("unknown transaction") + base, version, data, previous = row + if previous == status: + return + if previous != "open": + raise ValueError("transaction is no longer open") + if status == "committed" and version != base: + current = db.execute("SELECT version FROM catalog WHERE id=1").fetchone()[0] + if current != base: + raise ValueError("transaction conflict") + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version, data)) + db.execute("UPDATE transactions SET status=? WHERE id=?", (status, tx)) + + def catalog_transaction_commit( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData + ) -> None: + self._finish(attach_opaque_data, transaction_opaque_data, "committed") + + def catalog_transaction_rollback( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData + ) -> None: + self._finish(attach_opaque_data, transaction_opaque_data, "rolled_back") + + +class SchemaOptionsWorker(Worker): + """Serve the reference through normal protocol dispatch and HTTP sealing.""" + + catalog_interface = SchemaOptionsCatalog diff --git a/tests/catalog/statement_reference.py b/tests/catalog/statement_reference.py new file mode 100644 index 00000000..86f93e07 --- /dev/null +++ b/tests/catalog/statement_reference.py @@ -0,0 +1,662 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103, D107 +"""Finite transactional statement example over the existing query executor. + +Run with an isolated VGI_STATEMENT_REFERENCE_DIR and ``python -m +tests.catalog.statement_reference --port 0``. One process owns all DuckDB +connections and receipts; this is NOT a portable multi-process provider, SQL +sandbox, retry service or recovery ledger. A process loss loses these receipts. +Only the exact SQL constants below are supported. There is no native DuckDB +write integration, general DDL/DML parser, external effect or transaction SQL. + +Prepare/bind describe known statements without executing them. Start executes +once on the transaction's real connection and materializes bounded fragments. +EOS completes a statement, not its transaction. Pending, cancelled, failed or +expired executions prevent commit; the caller must roll back the whole +transaction. Normal transaction end/detach/expiry releases all owned resources. +""" + +from __future__ import annotations + +import argparse +import os +import threading +import time +import uuid +from collections.abc import Iterator, Sequence +from contextlib import contextmanager +from dataclasses import dataclass, field, replace +from pathlib import Path +from typing import Any, ClassVar, Literal, overload +from wsgiref.simple_server import make_server + +import duckdb +import pyarrow as pa +from vgi_rpc.rpc import CallContext + +from vgi.catalog import ( + AttachOpaqueData, + CatalogAttachResult, + FunctionInfo, + IndexInfo, + MacroInfo, + ReadOnlyCatalogInterface, + SchemaInfo, + SchemaObjectType, + SerializedSchema, + TableInfo, + TransactionOpaqueData, + ViewInfo, +) +from vgi.catalog.query import ( + QUERY_ENCODING, + QUERY_EXPLICIT, + QUERY_STATEMENT, + STATEMENT_RESULT_KEY, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, + QueryEvaluationContext, +) +from vgi.protocol import ClientCapabilities +from vgi.query_parameters import PARAMETER_MODE_KEY, QueryParameters, read_parameter_schema +from vgi.query_provider import CatalogQueryProvider, QueryExecution, QueryOwner, QueryPreparation +from vgi.schema_path import SchemaPath +from vgi.worker import Worker + +from .typing_helpers import present + +INSERT = "INSERT INTO items VALUES ($id, $label)" +INSERT_RETURNING = INSERT + " RETURNING id, label" +UPDATE = "UPDATE items SET label = $label WHERE id = $id" +UPDATE_RETURNING = "UPDATE items SET label = $label RETURNING id, label" +DELETE = "DELETE FROM items WHERE id = $id" +CREATE = "CREATE TABLE added (value BIGINT)" +DROP = "DROP TABLE added" +SELECT = "SELECT id, label FROM items ORDER BY id" +BASELINE = [(0, "zero"), (1, "one"), (2, "two")] +ROW_FIELDS: list[pa.Field[Any]] = [pa.field("id", pa.int64()), pa.field("label", pa.string())] +ROWS = pa.schema(ROW_FIELDS) +COMMAND = pa.schema([("affected_rows", pa.int64())], metadata={STATEMENT_RESULT_KEY: b"command"}) +RETURNING = ROWS.with_metadata({STATEMENT_RESULT_KEY: b"rows"}) +TYPES = {pa.int64(): duckdb.sqltypes.BIGINT, pa.string(): duckdb.sqltypes.VARCHAR} +REVISION = "finite-statement-reference-1" +SIGNING_KEY = b"statement-reference-test-key" +OFFER = ClientQueryCapabilities( + encodings=[QUERY_ENCODING], semantic_profiles=[QUERY_EXPLICIT, QUERY_STATEMENT], engine_version="fixture" +) + + +@dataclass(frozen=True) +class _Statement: + parameters: tuple[tuple[str, pa.DataType], ...] + output: pa.Schema + ddl: bool = False + read_only: bool = False + + +STATEMENTS = { + INSERT: _Statement((("id", pa.int64()), ("label", pa.string())), COMMAND), + INSERT_RETURNING: _Statement((("id", pa.int64()), ("label", pa.string())), RETURNING), + UPDATE: _Statement((("id", pa.int64()), ("label", pa.string())), COMMAND), + UPDATE_RETURNING: _Statement((("label", pa.string()),), RETURNING), + DELETE: _Statement((("id", pa.int64()),), COMMAND), + CREATE: _Statement((), COMMAND, ddl=True), + DROP: _Statement((), COMMAND, ddl=True), + SELECT: _Statement((), ROWS, read_only=True), +} + + +def now_ms() -> int: + return time.time_ns() // 1_000_000 + + +@dataclass +class _Transaction: + attach: bytes + connection: duckdb.DuckDBPyConnection + expires: int + failed: bool = False + pending: set[bytes] = field(default_factory=set) + started: set[bytes] = field(default_factory=set) + + +@dataclass +class _Result: + scope: bytes + transaction: bytes + batches: list[pa.RecordBatch] + expires: int + fail_read_at: int | None + eof: bool = False + + +@dataclass(frozen=True) +class _Completion: + attach: bytes + operation: str + expires: int + + +class StatementAuthority: + """A bounded, locked owner of real DuckDB transactions in one process.""" + + lifetime_ms = 120_000 + max_resources = 64 + max_result_rows = 10_000 + max_result_bytes = 1024 * 1024 + + def __init__(self, path: Path) -> None: + self.lock = threading.RLock() + self.connection = duckdb.connect(str(path), config={"enable_external_access": False, "threads": 1}) + self.connection.execute("CREATE TABLE IF NOT EXISTS _statement_version (version BIGINT)") + row = self.connection.execute("SELECT count(*) FROM _statement_version").fetchone() + assert row is not None + if row[0] == 0: + self.connection.execute("INSERT INTO _statement_version VALUES (1)") + self.connection.execute("CREATE TABLE items (id BIGINT, label VARCHAR)") + self.connection.executemany("INSERT INTO items VALUES (?, ?)", BASELINE) + self.attachments: set[bytes] = set() + self.transactions: dict[bytes, _Transaction] = {} + # In-process idempotency only, bounded together with active transactions. + # No receipt survives process loss, detach or this fixture's finite lease. + self.completed: dict[bytes, _Completion] = {} + self.preparations: dict[bytes, tuple[bytes, bytes, QueryPreparation]] = {} + self.executions: dict[bytes, _Result] = {} + self.starts = 0 + # Test-only fault is copied into the next execution, never looked up on replay. + self.fail_read_at: int | None = None + + def authorize(self, attach: bytes) -> None: + if attach not in self.attachments: + raise ValueError("statement attachment is closed") + + def transaction(self, attach: bytes, token: bytes | None, *, allow_failed: bool = False) -> _Transaction: + self.authorize(attach) + tx = None if token is None else self.transactions.get(token) + if tx is None or tx.attach != attach: + raise ValueError("statement transaction is inactive or belongs to another attachment") + if tx.expires <= now_ms(): + tx.failed = True + if tx.failed and not allow_failed: + raise ValueError("statement transaction failed; rollback required") + return tx + + @contextmanager + def reader(self, attach: bytes, token: bytes | None = None) -> Iterator[duckdb.DuckDBPyConnection]: + with self.lock: + self.authorize(attach) + if token is not None: + yield self.transaction(attach, token).connection + else: + connection = self.connection.cursor() + try: + yield connection + finally: + connection.close() + + def remove_transaction(self, token: bytes) -> None: + tx = self.transactions.pop(token) + tx.connection.close() + self.preparations = {key: value for key, value in self.preparations.items() if value[1] != token} + self.executions = {key: value for key, value in self.executions.items() if value.transaction != token} + + def sweep(self) -> None: + with self.lock: + for token, tx in list(self.transactions.items()): + if tx.expires <= now_ms(): + tx.connection.execute("ROLLBACK") + self.remove_transaction(token) + self.preparations = { + key: value + for key, value in self.preparations.items() + if (value[2].result.expires_at_epoch_ms or 0) > now_ms() + } + self.completed = {key: value for key, value in self.completed.items() if value.expires > now_ms()} + for handle, result in list(self.executions.items()): + if result.expires <= now_ms(): + tx = self.transactions[result.transaction] + tx.failed = True + tx.pending.discard(handle) + del self.executions[handle] + + def close(self) -> None: + with self.lock: + for token in list(self.transactions): + self.transactions[token].connection.execute("ROLLBACK") + self.remove_transaction(token) + self.connection.close() + + +class StatementReferenceProvider(CatalogQueryProvider): + """Describe finite SQL, then execute it on its owner's transaction connection.""" + + def __init__(self, authority: StatementAuthority) -> None: + self.authority = authority + + def capabilities(self, *, owner: QueryOwner, offer: ClientQueryCapabilities) -> QueryCapabilities | None: + with self.authority.lock: + self.authority.authorize(owner.attach_opaque_data) + return QueryCapabilities( + encoding=QUERY_ENCODING, + semantic_profiles=[QUERY_EXPLICIT, QUERY_STATEMENT], + execution_modes=["explicit", "statement"], + backend_engine="duckdb", + backend_version=duckdb.__version__, + capability_revision=REVISION, + default_evaluation_context=QueryEvaluationContext(), + ) + + def prepare(self, *, owner: QueryOwner, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + authority = self.authority + with authority.lock: + authority.sweep() + tx = authority.transaction(owner.attach_opaque_data, owner.transaction_opaque_data) + spec = STATEMENTS.get(request.query) + if spec is None or request.mode != ("explicit" if spec.read_only else "statement"): + return CatalogQueryPrepareResult(status="declined", reason_code="unsupported_statement") + expected = pa.schema(spec.parameters, metadata={PARAMETER_MODE_KEY: b"named"}) + if not read_parameter_schema(request.parameter_schema).equals(expected, check_metadata=True): + raise ValueError("statement parameter schema must match exact declared names, types and metadata") + if len(authority.preparations) >= authority.max_resources: + raise ValueError("statement preparation quota exceeded") + # Only metadata lookup: no SQL from request is run during prepare/bind. + row = tx.connection.execute("SELECT version FROM _statement_version").fetchone() + assert row is not None + version = int(row[0]) + result = CatalogQueryPrepareResult( + status="accepted", + prepared_query_token=uuid.uuid4().bytes, + output_schema=spec.output.serialize().to_pybytes(), + result_ordering="query_order" if spec.read_only else "unspecified", + validated_catalog_version=version, + context_digest=request.evaluation_context.digest(request.encoding, request.semantic_profile), + expires_at_epoch_ms=min(tx.expires, now_ms() + authority.lifetime_ms), + ) + assert result.prepared_query_token is not None + assert owner.transaction_opaque_data is not None + preparation = QueryPreparation(request=request, result=result) + authority.preparations[result.prepared_query_token] = ( + owner.scope, + owner.transaction_opaque_data, + preparation, + ) + return result + + def resolve(self, *, owner: QueryOwner, recipe: bytes) -> QueryPreparation: + with self.authority.lock: + self.authority.transaction(owner.attach_opaque_data, owner.transaction_opaque_data) + saved = self.authority.preparations.get(recipe) + if saved is None or saved[0] != owner.scope or (saved[2].result.expires_at_epoch_ms or 0) <= now_ms(): + raise ValueError("statement preparation is missing, expired or belongs to another owner") + return saved[2] + + def start( + self, *, owner: QueryOwner, preparation: QueryPreparation, parameters: QueryParameters, execution_id: bytes + ) -> QueryExecution: + authority = self.authority + with authority.lock: + assert preparation.result.prepared_query_token is not None + assert owner.transaction_opaque_data is not None + saved = self.resolve(owner=owner, recipe=preparation.result.prepared_query_token) + if saved != preparation: + raise ValueError("statement preparation changed") + parameters.validate_prepared_schema(saved.request.parameter_schema) + if parameters.batch.nbytes > 8192: + raise ValueError("statement parameter values exceed this finite provider's 8 KiB limit") + tx = authority.transaction(owner.attach_opaque_data, owner.transaction_opaque_data) + if tx.pending: + raise ValueError("statement result is still pending; consume EOS or rollback") + if execution_id in tx.started: + raise ValueError("statement execution cannot be restarted") + if len(tx.started) >= authority.max_resources: + raise ValueError("statement execution quota exceeded") + tx.started.add(execution_id) + spec = STATEMENTS[saved.request.query] + try: + authority.starts += 1 + values = { + definition.name: duckdb.Value(parameters.batch.column(i)[0].as_py(), TYPES[definition.type]) + for i, definition in enumerate(parameters.schema) + } + cursor = tx.connection.execute(saved.request.query, values) + if spec.output == COMMAND: + row = None if spec.ddl else cursor.fetchone() + assert spec.ddl or row is not None + count = None if row is None else row[0] + cursor.fetchall() + batches = [pa.record_batch([pa.array([count], type=pa.int64())], schema=COMMAND)] + if spec.ddl: + tx.connection.execute("UPDATE _statement_version SET version=version+1") + else: + batches = [] + rows = 0 + result_bytes = 0 + while chunk := cursor.fetchmany(2): + rows += len(chunk) + if rows > authority.max_result_rows: + raise ValueError("statement result row quota exceeded") + batch = pa.record_batch( + [ + pa.array(column, type=f.type) + for column, f in zip(zip(*chunk, strict=True), spec.output, strict=True) + ], + schema=spec.output, + ) + result_bytes += batch.nbytes + if result_bytes > authority.max_result_bytes: + raise ValueError("statement result byte quota exceeded") + batches.append(batch) + if sum(batch.nbytes for batch in batches) > authority.max_result_bytes: + raise ValueError("statement result byte quota exceeded") + handle = uuid.uuid4().bytes + expires = min(tx.expires, now_ms() + authority.lifetime_ms) + authority.executions[handle] = _Result( + owner.scope, owner.transaction_opaque_data, batches, expires, authority.fail_read_at + ) + authority.fail_read_at = None + tx.pending.add(handle) + return QueryExecution(handle=handle, expires_at_epoch_ms=expires) + except Exception: + tx.failed = True + raise + + def read(self, *, owner: QueryOwner, handle: bytes, position: int) -> pa.RecordBatch | None: + with self.authority.lock: + tx = self.authority.transaction(owner.attach_opaque_data, owner.transaction_opaque_data) + result = self.authority.executions.get(handle) + if result is None or result.scope != owner.scope: + raise ValueError("statement execution is missing; it cannot be restarted") + if result.expires <= now_ms() or not 0 <= position <= len(result.batches): + tx.failed = True + raise ValueError("statement execution expired or result position is invalid") + if position == result.fail_read_at: + tx.failed = True + raise RuntimeError("injected statement result failure after execution") + if position == len(result.batches): + result.eof = True + return None + return result.batches[position] + + def close(self, *, owner: QueryOwner, handle: bytes, reason: str) -> None: + with self.authority.lock: + result = self.authority.executions.get(handle) + if result is None: + return + if result.scope != owner.scope: + raise ValueError("statement execution belongs to another owner") + tx = self.authority.transaction(owner.attach_opaque_data, owner.transaction_opaque_data, allow_failed=True) + if reason != "eos" or not result.eof: + tx.failed = True + tx.pending.discard(handle) + del self.authority.executions[handle] + + def release(self, *, owner: QueryOwner, recipe: bytes) -> None: + with self.authority.lock: + saved = self.authority.preparations.get(recipe) + if saved is not None: + if saved[0] != owner.scope: + raise ValueError("statement preparation belongs to another owner") + del self.authority.preparations[recipe] + + +class StatementReferenceCatalog(ReadOnlyCatalogInterface): + """Catalog transaction methods own statement effects, metadata and cleanup.""" + + catalog_name = "statements" + catalog_version_frozen = False + catalog_contents_attach_independent = False + authority: ClassVar[StatementAuthority | None] = None + + def __init__(self) -> None: + super().__init__() + if type(self).authority is None: + root = Path(os.environ["VGI_STATEMENT_REFERENCE_DIR"]) + if not root.is_absolute(): + raise ValueError("VGI_STATEMENT_REFERENCE_DIR must name an isolated absolute directory") + root.mkdir(parents=True, exist_ok=True) + type(self).authority = StatementAuthority(root / "data.duckdb") + assert type(self).authority is not None + self._authority = present(type(self).authority) + self.query_provider = StatementReferenceProvider(self._authority) + + def catalog_attach( + self, + *, + name: str, + options: dict[str, Any], + data_version_spec: str | None, + implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, + ctx: CallContext | None = None, + ) -> CatalogAttachResult: + result = super().catalog_attach( + name=name, + options=options, + data_version_spec=data_version_spec, + implementation_version=implementation_version, + client_capabilities=client_capabilities, + ctx=ctx, + ) + with self._authority.lock: + self._authority.sweep() + if len(self._authority.attachments) >= self._authority.max_resources: + raise ValueError("statement attachment quota exceeded") + attach = AttachOpaqueData(uuid.uuid4().bytes) + self._authority.attachments.add(attach) + return replace( + result, + attach_opaque_data=attach, + attach_opaque_data_required=True, + supports_transactions=True, + supports_catalog_contents=False, + catalog_version_frozen=False, + catalog_version=self.catalog_version(attach_opaque_data=attach), + ) + + def catalog_transaction_begin(self, *, attach_opaque_data: AttachOpaqueData) -> TransactionOpaqueData: + with self._authority.lock: + self._authority.sweep() + self._authority.authorize(attach_opaque_data) + if len(self._authority.transactions) + len(self._authority.completed) >= self._authority.max_resources: + raise ValueError("statement transaction quota exceeded") + connection = self._authority.connection.cursor() + try: + connection.execute("BEGIN") + except Exception: + connection.close() + raise + token = TransactionOpaqueData(uuid.uuid4().bytes) + self._authority.transactions[token] = _Transaction( + attach_opaque_data, connection, now_ms() + self._authority.lifetime_ms + ) + return token + + def _finish(self, attach: bytes, token: bytes, operation: str) -> None: + with self._authority.lock: + self._authority.authorize(attach) + completed = self._authority.completed.get(token) + if completed is not None: + if completed.attach != attach or completed.expires <= now_ms(): + raise ValueError("statement completion is expired or belongs to another attachment") + if completed.operation != operation: + raise ValueError("statement transaction already completed with the opposite operation") + return + tx = self._authority.transaction(attach, token, allow_failed=True) + if operation == "COMMIT" and (tx.failed or tx.pending): + raise ValueError("statement transaction failed or result is pending; rollback required") + try: + tx.connection.execute(operation) + except Exception: + tx.failed = True + raise + self._authority.remove_transaction(token) + self._authority.completed[token] = _Completion(attach, operation, now_ms() + self._authority.lifetime_ms) + + def catalog_transaction_commit( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData + ) -> None: + self._finish(attach_opaque_data, transaction_opaque_data, "COMMIT") + + def catalog_transaction_rollback( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData + ) -> None: + self._finish(attach_opaque_data, transaction_opaque_data, "ROLLBACK") + + def catalog_detach(self, *, attach_opaque_data: AttachOpaqueData) -> None: + with self._authority.lock: + for token, tx in list(self._authority.transactions.items()): + if tx.attach == attach_opaque_data: + self._finish(attach_opaque_data, token, "ROLLBACK") + self._authority.completed = { + key: value for key, value in self._authority.completed.items() if value.attach != attach_opaque_data + } + self._authority.attachments.discard(attach_opaque_data) + + def catalog_version( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None = None, + ctx: CallContext | None = None, + ) -> int: + with self._authority.reader(attach_opaque_data, transaction_opaque_data) as connection: + row = connection.execute("SELECT version FROM _statement_version").fetchone() + assert row is not None + return int(row[0]) + + def table_get( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: SchemaPath, + name: str, + at_unit: str | None = None, + at_value: str | None = None, + ) -> TableInfo | None: + if schema_path != ["main"] or name not in {"items", "added"}: + return None + if at_unit is not None or at_value is not None: + raise ValueError("statement reference does not support time travel") + with self._authority.reader(attach_opaque_data, transaction_opaque_data) as connection: + if not connection.execute( + "SELECT 1 FROM information_schema.tables WHERE table_schema='main' AND table_name=?", [name] + ).fetchone(): + return None + schema = ROWS if name == "items" else pa.schema([("value", pa.int64())]) + return TableInfo( + name=name, + schema_path=["main"], + columns=SerializedSchema(schema.serialize().to_pybytes()), + constraints=[], + comment=None, + tags={}, + ) + + def schemas( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None + ) -> list[SchemaInfo]: + self.catalog_version(attach_opaque_data=attach_opaque_data, transaction_opaque_data=transaction_opaque_data) + return [SchemaInfo(path=["main"], comment=None, tags={})] + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[SchemaObjectType.TABLE], + ) -> Sequence[TableInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[SchemaObjectType.VIEW], + ) -> Sequence[ViewInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[ + SchemaObjectType.SCALAR_FUNCTION, + SchemaObjectType.TABLE_FUNCTION, + SchemaObjectType.AGGREGATE_FUNCTION, + ], + ) -> Sequence[FunctionInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[SchemaObjectType.SCALAR_MACRO, SchemaObjectType.TABLE_MACRO], + ) -> Sequence[MacroInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: Literal[SchemaObjectType.INDEX], + ) -> Sequence[IndexInfo]: ... + + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + type: SchemaObjectType, + ) -> Sequence[TableInfo | ViewInfo | FunctionInfo | MacroInfo | IndexInfo]: + if path != ["main"] or type != SchemaObjectType.TABLE: + return [] + return [ + info + for name in ["items", "added"] + if ( + info := self.table_get( + attach_opaque_data=attach_opaque_data, + transaction_opaque_data=transaction_opaque_data, + schema_path=path, + name=name, + ) + ) + is not None + ] + + +class StatementReferenceWorker(Worker): + """Use ordinary public query/transaction dispatch, including signed HTTP.""" + + catalog_interface = StatementReferenceCatalog + + +def main() -> None: + from vgi.serve import create_app + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--port", type=int, default=0) + args = parser.parse_args() + app = create_app(StatementReferenceWorker, describe=False, signing_key=SIGNING_KEY) + with make_server("127.0.0.1", args.port, app) as server: + print(f"PORT:{server.server_port}", flush=True) + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/tests/catalog/table_layout_reference.py b/tests/catalog/table_layout_reference.py new file mode 100644 index 00000000..b3a64987 --- /dev/null +++ b/tests/catalog/table_layout_reference.py @@ -0,0 +1,277 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D107 + +"""Persistent table-option/layout reference with real existing-row rewrites. + +The fixture uses the pinned 1.5 SQL parser locally to prove a closed scalar +expression profile. Its AST is only a validation aid and never crosses VGI. +It advertises schema/table DDL transactions, not general data-write atomicity. +""" + +from __future__ import annotations + +import base64 +import json +import sqlite3 +from collections.abc import Sequence +from contextlib import closing +from dataclasses import replace +from typing import Any, Literal, Never, overload + +import duckdb +import pyarrow as pa + +from vgi.catalog import SchemaInfo, TableInfo +from vgi.catalog.capabilities import ( + ROW_SCALAR_EXPRESSION, + TABLE_LAYOUT_ALTER_FUTURE, + TABLE_LAYOUT_ALTER_REWRITE, + TABLE_LAYOUT_CREATE, + TABLE_LAYOUT_READ, + TABLE_METADATA_CREATE, + TABLE_OPTIONS_ALTER, + TABLE_OPTIONS_CREATE, + TABLE_OPTIONS_READ, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import ( + AttachOpaqueData, + CatalogContentsResult, + OnConflict, + SchemaContentsInfo, + SchemaObjectType, + TransactionOpaqueData, +) +from vgi.catalog.layout import LayoutChangeMode, TableLayout, TableLayoutChange +from vgi.schema_path import schema_path_key +from vgi.worker import Worker + +from .expression_reference import validate_expression as _validate_expression +from .schema_options_reference import DEFINITIONS, PROFILES, SchemaOptionsCatalog + +TABLE_PROFILES = CatalogCapabilities( + read=sorted([*PROFILES.read, TABLE_OPTIONS_READ, TABLE_LAYOUT_READ, ROW_SCALAR_EXPRESSION]), + ddl=sorted( + [ + *PROFILES.ddl, + TABLE_OPTIONS_CREATE, + TABLE_OPTIONS_ALTER, + TABLE_METADATA_CREATE, + TABLE_LAYOUT_CREATE, + TABLE_LAYOUT_ALTER_FUTURE, + TABLE_LAYOUT_ALTER_REWRITE, + ] + ), + transaction=PROFILES.transaction, +) + + +def _table_key(path: list[str], name: str) -> str: + if not name or "\x00" in name: + raise ValueError("invalid table name") + return json.dumps({"schema": schema_path_key(path), "table": name.lower()}) + + +def arrange_rows(rows: list[dict[str, Any]], info: TableInfo) -> list[dict[str, Any]]: + """Validate/bind keys, then partition and sort actual rows in one snapshot.""" + columns = pa.ipc.read_schema(pa.py_buffer(info.columns)) + layout = info.layout or TableLayout() + partitions, sorts = layout.partition_expressions(), layout.sort_keys() + with duckdb.connect(config={"enable_external_access": False}) as connection: + connection.register("input_data", pa.Table.from_pylist(rows, schema=columns)) + for expr in partitions + [key.expression for key in sorts]: + _validate_expression(connection, expr, info.schema_path) + select = "*" + "".join(f", ({expr.sql}) AS __vgi_partition_{i}" for i, expr in enumerate(partitions)) + order = ", ".join(f"({key.expression.sql}) {key.direction.value} NULLS {key.null_order.value}" for key in sorts) + result = connection.execute( + f"SELECT {select} FROM input_data" + (f" ORDER BY {order}" if order else "") + ).to_arrow_table() + groups: dict[str, dict[str, Any]] = {} + for row in range(len(result)): + key = [result.column(len(columns) + i)[row].as_py() for i in range(len(partitions))] + group = groups.setdefault(json.dumps(key), {"key": key, "rows": []}) + group["rows"].append({name: result.column(i)[row].as_py() for i, name in enumerate(columns.names)}) + return list(groups.values()) + + +class TableLayoutCatalog(SchemaOptionsCatalog): + """Schema/table metadata and data-layout snapshots in one durable transaction.""" + + catalog_name = "layout" + profiles = TABLE_PROFILES + + def schemas(self, *, attach_opaque_data: Any, transaction_opaque_data: Any) -> list[SchemaInfo]: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + return [self._decode(value, attach_opaque_data) for key, value in data.items() if key.startswith("[")] + + @staticmethod + def _info(value: str) -> TableInfo: + return TableInfo.deserialize_from_bytes(base64.b64decode(json.loads(value)["info"])) + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + type: Literal[SchemaObjectType.TABLE], + ) -> list[TableInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + type: Literal[ + SchemaObjectType.VIEW, + SchemaObjectType.SCALAR_FUNCTION, + SchemaObjectType.TABLE_FUNCTION, + SchemaObjectType.AGGREGATE_FUNCTION, + SchemaObjectType.SCALAR_MACRO, + SchemaObjectType.TABLE_MACRO, + SchemaObjectType.INDEX, + ], + ) -> list[Never]: ... + + def schema_contents( + self, *, attach_opaque_data: Any, transaction_opaque_data: Any, path: list[str], type: SchemaObjectType + ) -> Sequence[TableInfo]: + if type != SchemaObjectType.TABLE: + return [] + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + return [ + self._info(value) + for key, value in data.items() + if key.startswith("{") and schema_path_key(self._info(value).schema_path) == schema_path_key(path) + ] + + def table_get( + self, *, attach_opaque_data: Any, transaction_opaque_data: Any, schema_path: list[str], name: str, **kwargs: Any + ) -> TableInfo | None: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + value = data.get(_table_key(schema_path, name)) + return self._info(value) if value is not None else None + + def catalog_contents(self, *, attach_opaque_data: Any, if_none_match: str | None = None) -> CatalogContentsResult: + return CatalogContentsResult( + schemas=[ + SchemaContentsInfo( + schema=info, + tables=self.schema_contents( + attach_opaque_data=attach_opaque_data, + transaction_opaque_data=None, + path=info.path, + type=SchemaObjectType.TABLE, + ), + ) + for info in self.schemas(attach_opaque_data=attach_opaque_data, transaction_opaque_data=None) + ], + etag="same-provider-etag", + ) + + def table_option_definitions(self, **kwargs: Any) -> Any: + return DEFINITIONS + + def _arrange(self, rows: list[dict[str, Any]], info: TableInfo) -> list[dict[str, Any]]: + layout = info.layout or TableLayout() + for expression in layout.partition_expressions() + [key.expression for key in layout.sort_keys()]: + if expression.profile not in self.profiles.read: + raise ValueError("expression profile is no longer supported") + return arrange_rows(rows, info) + + def _create_constraints(self, args: dict[str, Any]) -> tuple[list[bytes], bytes | None]: + """Keep this layout-only reference explicit about unsupported enforcement.""" + if args["constraint_definitions"]: + raise ValueError("constraint enforcement is outside this reference") + return [], None + + def _change(self, args: dict[str, Any], action: str) -> None: + attach, tx = args["attach_opaque_data"], args["transaction_opaque_data"] + path, name = args["schema_path"], args["name"] + key = _table_key(path, name) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, tx) + if action == "create": + if json.dumps(schema_path_key(path)) not in data: + raise ValueError("schema does not exist") + constraints, revision = self._create_constraints(args) + info = TableInfo( + name=name, + schema_path=path, + columns=args["columns"], + comment=args.get("comment"), + tags=args.get("tags") or {}, + constraints=constraints, + definition_revision=revision, + options=DEFINITIONS.create(args.get("options")), + layout=args.get("layout") or TableLayout(), + ) + physical = self._arrange([], info) + if key in data: + if args["on_conflict"] == OnConflict.IGNORE: + return + if args["on_conflict"] != OnConflict.REPLACE: + raise ValueError("table already exists") + else: + if key not in data: + raise ValueError("table does not exist") + record = json.loads(data[key]) + info, physical = self._info(data[key]), record["partitions"] + if action == "options": + assert info.options is not None + info = replace(info, options=DEFINITIONS.apply(info.options, args["changes"])) + elif action == "layout": + change: TableLayoutChange = args["changes"] + info = replace(info, layout=change.apply_to(info.layout or TableLayout())) + self._arrange([], info) # Bind and validate even a future-write change. + if change.mode == LayoutChangeMode.REWRITE: + physical = self._arrange([row for part in physical for row in part["rows"]], info) + elif action == "seed": + physical += self._arrange(args["rows"], info) + elif action == "drop": + del data[key] + if action != "drop": + data[key] = json.dumps( + {"info": base64.b64encode(info.serialize_to_bytes()).decode(), "partitions": physical} + ) + if tx is None: + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version + 1, json.dumps(data))) + else: + db.execute("UPDATE transactions SET version=?, data=? WHERE id=?", (version + 1, json.dumps(data), tx)) + + def table_create(self, **kwargs: Any) -> None: + self._change(kwargs, "create") + + def table_alter_options(self, **kwargs: Any) -> None: + self._change(kwargs, "options") + + def table_alter_layout(self, **kwargs: Any) -> None: + self._change(kwargs, "layout") + + def table_drop(self, **kwargs: Any) -> None: + self._change(kwargs, "drop") + + def seed_rows(self, **kwargs: Any) -> None: + """Populate physical data for layout tests without advertising data writes.""" + self._change(kwargs, "seed") + + def physical_rows( + self, *, attach_opaque_data: Any, transaction_opaque_data: Any, schema_path: list[str], name: str + ) -> list[dict[str, Any]]: + """Read the durable physical partitions for a test assertion.""" + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + return json.loads(data[_table_key(schema_path, name)])["partitions"] # type: ignore[no-any-return] + + +class TableLayoutWorker(Worker): + """Normal public protocol host for the table-layout reference.""" + + catalog_interface = TableLayoutCatalog diff --git a/tests/catalog/test_catalog_interface.py b/tests/catalog/test_catalog_interface.py index 40d74d6d..f7f9b904 100644 --- a/tests/catalog/test_catalog_interface.py +++ b/tests/catalog/test_catalog_interface.py @@ -37,7 +37,11 @@ OrderPreservation, ReadOnlyCatalogInterface, ) +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.macros import normalize_macro_overloads +from vgi.catalog.metadata_changes import CatalogMetadataChanges from vgi.exceptions import CatalogReadOnlyError +from vgi.protocol import ClientCapabilities # Common test data TEST_ATTACH_ID = AttachOpaqueData(b"test") @@ -81,6 +85,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: "ClientCapabilities | None" = None, ctx: Any = None, ) -> CatalogAttachResult: """Attach to catalog.""" @@ -237,6 +242,16 @@ def _not_implemented_test_cases() -> list[tuple[str, str, Callable[[MinimalCatal tags={}, ), ), + ( + "schema_alter_metadata", + "Schema metadata alteration not implemented", + lambda c: c.schema_alter_metadata( + attach_opaque_data=TEST_ATTACH_ID, + transaction_opaque_data=None, + path=["main"], + changes=CatalogMetadataChanges(), + ), + ), ( "schema_drop", "Schema drop not implemented", @@ -268,9 +283,7 @@ def _not_implemented_test_cases() -> list[tuple[str, str, Callable[[MinimalCatal name="table", columns=SerializedSchema(b""), on_conflict=OnConflict.ERROR, - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraint_definitions=[], ), ), ( @@ -294,9 +307,16 @@ def _not_implemented_test_cases() -> list[tuple[str, str, Callable[[MinimalCatal schema_path=["main"], name="my_macro", macro_type=MacroType.SCALAR, - parameters=["x"], - definition="x * 2", on_conflict=OnConflict.ERROR, + overloads=normalize_macro_overloads( + macro_type=(MacroType.SCALAR).value, + schema_path=["main"], + parameters=["x"], + definition="x * 2", + parameter_default_values=None, + arguments_schema=None, + evaluation_context=QueryEvaluationContext(), + ), ), ), ( @@ -347,6 +367,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: "ClientCapabilities | None" = None, ctx: Any = None, ) -> CatalogAttachResult: """Attach to catalog.""" @@ -441,6 +462,15 @@ def _readonly_test_cases() -> list[tuple[str, Callable[[MinimalReadOnlyCatalog], tags={}, ), ), + ( + "schema_alter_metadata", + lambda c: c.schema_alter_metadata( + attach_opaque_data=TEST_ATTACH_ID, + transaction_opaque_data=None, + path=["main"], + changes=CatalogMetadataChanges(), + ), + ), ( "schema_drop", lambda c: c.schema_drop( @@ -460,9 +490,7 @@ def _readonly_test_cases() -> list[tuple[str, Callable[[MinimalReadOnlyCatalog], name="table", columns=SerializedSchema(b""), on_conflict=OnConflict.ERROR, - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraint_definitions=[], ), ), ( @@ -515,9 +543,16 @@ def _readonly_test_cases() -> list[tuple[str, Callable[[MinimalReadOnlyCatalog], schema_path=["main"], name="my_macro", macro_type=MacroType.SCALAR, - parameters=["x"], - definition="x * 2", on_conflict=OnConflict.ERROR, + overloads=normalize_macro_overloads( + macro_type=(MacroType.SCALAR).value, + schema_path=["main"], + parameters=["x"], + definition="x * 2", + parameter_default_values=None, + arguments_schema=None, + evaluation_context=QueryEvaluationContext(), + ), ), ), ( diff --git a/tests/catalog/test_catalog_publication.py b/tests/catalog/test_catalog_publication.py index 0d17e14e..c2219ae2 100644 --- a/tests/catalog/test_catalog_publication.py +++ b/tests/catalog/test_catalog_publication.py @@ -319,14 +319,14 @@ def test_collections_consume_multiple_batches_and_reject_wrong_schema() -> None: class _S3Error(Exception): - def __init__(self, code: str): + def __init__(self, code: str) -> None: self.response = {"Error": {"Code": code}} class _S3: """A conditional object store fixture shared by separate repository instances.""" - def __init__(self, store: _Storage): + def __init__(self, store: _Storage) -> None: self.store = store self.etags: dict[str, str] = {} @@ -641,3 +641,85 @@ def change_generation(*args: Any, **kwargs: Any) -> Any: assert repository.lookup(manifest.cache_key) is None finally: _close_policy(policy) + + +def test_worker_publications_preserve_capability_projection(storage: _Storage, monkeypatch: pytest.MonkeyPatch) -> None: + """Shared published metadata and conditional replies stay within the negotiated projection.""" + from vgi.catalog.capabilities import SCHEMA_OPTIONS_READ, CatalogCapabilities + from vgi.catalog.options import OptionState + from vgi.protocol import ClientCapabilities + from vgi.typed_values import TypedValueBatch + + repository = MemoryCatalogPublicationRepository(storage, profile="projection", stable_urls=True) + policy = replace( + _policy(repository), + identity=lambda req: SnapshotIdentity( + catalog_namespace="test", + source_revision="deploy-1", + catalog_version=req.catalog_version, + visibility_scope="shared", + ), + validator=lambda req: "raw-provider-etag", + ) + monkeypatch.setattr(ContentsProbeCatalog, "catalog_snapshot_policy", policy) + available = CatalogCapabilities(read=[SCHEMA_OPTIONS_READ]) + monkeypatch.setattr(ContentsProbeCatalog, "catalog_capabilities", lambda self, **kwargs: available) + original = ContentsProbeCatalog.catalog_contents + options = OptionState(values=TypedValueBatch(pa.record_batch([pa.array([7], type=pa.int32())], names=["limit"]))) + + def contents(self: Any, **kwargs: Any) -> Any: + result = original(self, **kwargs) + return replace( + result, schemas=[replace(item, schema=replace(item.schema, options=options)) for item in result.schemas] + ) + + monkeypatch.setattr(ContentsProbeCatalog, "catalog_contents", contents) + worker = ContentsProbeWorker() + worker._signing_key = b"k" * 32 + + def attach(read: list[str]) -> bytes: + return worker.catalog_attach( + CatalogAttachRequest( + name=CATALOG_PROBE, + options=None, + data_version_spec=None, + implementation_version=None, + client_capabilities=ClientCapabilities( + engine="test", + native_formats=[], + catalogs=[], + can_stream=True, + filter_encodings=[], + catalog=CatalogCapabilities(read=read), + ), + ) + ).attach_opaque_data + + try: + full = attach([SCHEMA_OPTIONS_READ]) + limited = attach([]) + full_ref = worker.catalog_contents(full) + limited_ref = worker.catalog_contents(limited) + assert isinstance(full_ref, ExternalRef) and isinstance(limited_ref, ExternalRef) + assert full_ref != limited_ref + assert storage.uploads == 2 + assert worker.catalog_contents(full) == full_ref + assert worker.catalog_contents(limited) == limited_ref + assert worker.catalog_contents(full, "raw-provider-etag") == full_ref + # The same explicitly shared visibility can still reuse an identical projection. + assert worker.catalog_contents(attach([SCHEMA_OPTIONS_READ])) == full_ref + assert storage.uploads == 2 + manager = worker._catalog_snapshot_manager(policy) + manifests = list(manager._validated.values()) + assert len(manifests) == 2 and len({m.etag for m in manifests}) == 2 + responses = [] + for raw, encoding in storage.objects.values(): + assert encoding is None + with pa.ipc.open_stream(raw) as reader: + payload = reader.read_all().column(0)[0].as_py() + responses.append(CatalogContentsResponse.deserialize_from_bytes(payload)) + values = [decode_inventory(response)[0].schema.options for response in responses] + assert sum(value is None for value in values) == 1 + assert next(value for value in values if value is not None).values.batch.to_pylist() == [{"limit": 7}] + finally: + _close_policy(policy) diff --git a/tests/catalog/test_client_capabilities.py b/tests/catalog/test_client_capabilities.py new file mode 100644 index 00000000..66185732 --- /dev/null +++ b/tests/catalog/test_client_capabilities.py @@ -0,0 +1,164 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Client capability offers reach catalog hooks and remain scoped to each attach.""" + +from __future__ import annotations + +import json +import time +from dataclasses import asdict, replace +from typing import Any, cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient +from vgi_rpc.rpc import AuthContext, CallContext +from vgi_rpc.utils import serialize_record_batch_bytes + +from vgi.attach_ticket import mint_attach_ticket +from vgi.catalog import ( + AttachOpaqueData, + CatalogAttachResult, + ReadOnlyCatalogInterface, + SchemaInfo, + TransactionOpaqueData, +) +from vgi.client import Client +from vgi.meta_worker import MetaWorker +from vgi.protocol import CatalogAttachRequest, ClientCapabilities, VgiProtocol +from vgi.serve import create_app +from vgi.worker import Worker + +from .typing_helpers import present + +_SIGNING_KEY = b"client-capabilities-test-key" +_NATIVE = ClientCapabilities( + engine="duckdb", + native_formats=["parquet"], + catalogs=["ducklake"], + can_stream=False, + filter_encodings=["vgi.filters.v2"], +) +_LIMITED = ClientCapabilities( + engine="test-engine", native_formats=[], catalogs=[], can_stream=False, filter_encodings=[] +) + + +class _CapabilityCatalog(ReadOnlyCatalogInterface): + """Expose the offer as metadata, carrying it in the sealed attachment.""" + + catalog_name = "capabilities" + catalog_contents_attach_independent = False + + def catalog_attach( + self, + *, + name: str, + options: dict[str, Any], + data_version_spec: str | None, + implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, + ctx: CallContext | None = None, + ) -> CatalogAttachResult: + """Persist the offer without storing client state on the catalog instance.""" + result = super().catalog_attach( + name=name, + options=options, + data_version_spec=data_version_spec, + implementation_version=implementation_version, + client_capabilities=client_capabilities, + ctx=ctx, + ) + payload = json.dumps(asdict(client_capabilities) if client_capabilities is not None else None).encode() + return replace(result, attach_opaque_data=AttachOpaqueData(payload), attach_opaque_data_required=True) + + def schemas( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None + ) -> list[SchemaInfo]: + """Read the original offer on any worker serving a later catalog call.""" + return [ + SchemaInfo( + attach_opaque_data=attach_opaque_data, path=["main"], comment=attach_opaque_data.decode(), tags={} + ) + ] + + +class _CapabilityWorker(Worker): + """Serve a capability-aware catalog through the normal worker dispatcher.""" + + catalog_interface = _CapabilityCatalog + + +@pytest.mark.parametrize("capabilities", [None, _NATIVE, _LIMITED]) +def test_python_client_forwards_capabilities_over_http(capabilities: ClientCapabilities | None) -> None: + """The public Python client and Arrow RPC preserve absent and explicit offers.""" + app = create_app(_CapabilityWorker, describe=False, signing_key=_SIGNING_KEY) + with Client.from_http("http://vgi.test", httpx_client=_SyncTestClient(app)) as client: + attach = client.catalog_attach( + name="capabilities", + data_version_spec=None, + implementation_version=None, + client_capabilities=capabilities, + ) + schemas = client.schemas(attach_opaque_data=attach.attach_opaque_data) + assert json.loads(present(schemas[0].comment)) == (asdict(capabilities) if capabilities is not None else None) + + +def test_capabilities_survive_reconnect_without_cross_attachment_leakage() -> None: + """Different offers survive another client attaching and a new worker instance.""" + app = create_app(_CapabilityWorker, describe=False, signing_key=_SIGNING_KEY) + with Client.from_http("http://vgi.test", httpx_client=_SyncTestClient(app)) as client: + attachments = [ + client.catalog_attach( + name="capabilities", + data_version_spec=None, + implementation_version=None, + client_capabilities=offer, + ) + for offer in (_NATIVE, _LIMITED, None) + ] + for attach, offer in zip(attachments, (_NATIVE, _LIMITED, None), strict=True): + snapshot = client.load_catalog(attach=attach) + assert json.loads(present(snapshot.schemas[0].schema.comment)) == ( + asdict(offer) if offer is not None else None + ) + + restarted_app = create_app(_CapabilityWorker, describe=False, signing_key=_SIGNING_KEY) + with Client.from_http("http://vgi.test", httpx_client=_SyncTestClient(restarted_app)) as client: + for attach, offer in zip(attachments, (_NATIVE, _LIMITED, None), strict=True): + snapshot = client.load_catalog(attach=attach) + assert json.loads(present(snapshot.schemas[0].schema.comment)) == ( + asdict(offer) if offer is not None else None + ) + + +@pytest.mark.parametrize("use_meta_worker", [False, True]) +@pytest.mark.parametrize("capabilities", [None, _NATIVE, _LIMITED]) +def test_ticket_redemption_preserves_redeeming_client_capabilities( + monkeypatch: pytest.MonkeyPatch, use_meta_worker: bool, capabilities: ClientCapabilities | None +) -> None: + """A ticket restores catalog/options while retaining the redeeming engine's offer.""" + auth = AuthContext(domain="test", authenticated=True, principal="alice") + monkeypatch.setattr("vgi.worker.current_auth", lambda: auth) + child = _CapabilityWorker(quiet=True) + child._signing_key = _SIGNING_KEY + worker = MetaWorker([child]) if use_meta_worker else child + now = int(time.time()) + ticket, _ = mint_attach_ticket( + _SIGNING_KEY, + principal="alice", + catalog_name="capabilities", + options_ipc=serialize_record_batch_bytes(pa.RecordBatch.from_pylist([{"probe": "ticket"}])), + issued_at=now, + expires_at=now + 600, + ) + request = CatalogAttachRequest( + name="ticket-alias", + options=pa.RecordBatch.from_pylist([{"vgi_attach_ticket": ticket}]), + data_version_spec=None, + implementation_version=None, + client_capabilities=capabilities, + ) + attach = worker.catalog_attach(CatalogAttachRequest.deserialize_from_bytes(request.serialize_to_bytes())) + schemas = cast(VgiProtocol, worker).catalog_schemas(attach_opaque_data=attach.attach_opaque_data).to_infos() + assert json.loads(present(schemas[0].comment)) == (asdict(capabilities) if capabilities is not None else None) diff --git a/tests/catalog/test_column_expression_cli.py b/tests/catalog/test_column_expression_cli.py new file mode 100644 index 00000000..8cefef29 --- /dev/null +++ b/tests/catalog/test_column_expression_cli.py @@ -0,0 +1,98 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""CLI defaults/USING reach the durable provider through real signed HTTP RPCs.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pyarrow as pa +import pytest +from click.testing import CliRunner +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.catalog.catalog_interface import AttachOpaqueData, SerializedSchema +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import SqlExpression, column_expressions +from vgi.client import Client +from vgi.client.cli import cli +from vgi.serve import create_app + +from .column_paths_reference import ColumnPathsCatalog, ColumnPathsWorker +from .typing_helpers import CatalogObjectArgs, CatalogObjectTransactionArgs, present + + +@pytest.mark.parametrize("automatic_attach", [False, True]) +def test_cli_column_expression_lifecycle( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, automatic_attach: bool +) -> None: + monkeypatch.setattr(ColumnPathsCatalog, "database_path", str(tmp_path / "cli-columns.sqlite"), raising=False) + key = b"column-cli-reference-signing-key" + backend = ColumnPathsWorker(quiet=True) + backend._signing_key = key + runner = CliRunner() + with Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(create_app(ColumnPathsWorker, describe=False, signing_key=key)) + ) as client: + # Replace only CLI transport construction; attach, codecs, capability + # negotiation and provider mutations still use the public HTTP path. + monkeypatch.setattr("vgi.client.cli_catalog.Client", lambda _: client) + monkeypatch.setattr("vgi.client.Client", lambda _: client) + attached = runner.invoke(cli, ["catalog", "attach", "columns", "--worker", "unused"]) + assert attached.exit_code == 0, attached.output + token_hex = json.loads(attached.output)["attach_opaque_data"] + token = AttachOpaqueData(bytes.fromhex(token_hex)) + args: CatalogObjectArgs = dict(attach_opaque_data=token, schema_path=["main"], name="cli.values") + client.table_create( + **args, columns=SerializedSchema(pa.schema([pa.field("a.b", pa.int32())]).serialize().to_pybytes()) + ) + provider_args: CatalogObjectTransactionArgs = dict( + attach_opaque_data=AttachOpaqueData(present(backend._unwrap_attach(token))), + schema_path=args["schema_path"], + name=args["name"], + transaction_opaque_data=None, + ) + backend._get_catalog().seed_rows(**provider_args, rows=[{"a.b": 3}]) + attachment = ["--catalog", "columns"] if automatic_attach else ["--attach-opaque-data", token_hex] + + def command(*arguments: str) -> None: + result = runner.invoke( + cli, + ["catalog", "table", "column", *arguments, *attachment, "--worker", "unused"], + ) + assert result.exit_code == 0, result.output + + def default() -> SqlExpression | None: + info = client.table_get(**args) + assert info is not None + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) + return column_expressions(schema.field("a.b")).default + + command("set-default", "main", "cli.values", "a.b", "9") + expression = default() + assert expression is not None and expression.sql == "9" + assert expression.schema_path == ["main"] + assert expression.evaluation_context == QueryEvaluationContext() + backend._get_catalog().seed_rows(**provider_args, rows=[{}]) + + # An absent --using stays absent; a present expression reads old rows. + command("set-type", "main", "cli.values", "--column", '{"name":"a.b","type":"int64"}') + command( + "set-type", + "main", + "cli.values", + "--column", + '{"name":"a.b","type":"float64"}', + "--using", + '"a.b" + 0.5', + ) + rows = [row for part in backend._get_catalog().physical_rows(**provider_args) for row in part["rows"]] + assert rows == [{"a.b": 3.5}, {"a.b": 9.5}] + + command("set-default", "main", "cli.values", "a.b", "NULL") + expression = default() + assert expression is not None and expression.sql == "NULL" + command("drop-default", "main", "cli.values", "a.b") + assert default() is None diff --git a/tests/catalog/test_column_metadata.py b/tests/catalog/test_column_metadata.py new file mode 100644 index 00000000..4af1c9ef --- /dev/null +++ b/tests/catalog/test_column_metadata.py @@ -0,0 +1,249 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Column comments/tags survive durable RPC edits and reject invalid metadata.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import replace +from typing import Any, cast + +import pyarrow as pa +import pytest + +from vgi.catalog import ( + ColumnMetadata, + OnConflict, + SchemaInfo, + Table, + TableInfo, + column_metadata, + validate_column_metadata, + with_column_metadata, +) +from vgi.catalog.catalog_interface import AttachOpaqueData, CatalogContentsResult, SchemaContentsInfo, SerializedSchema +from vgi.protocol import TableCreateRequest, TablesResponse + +from .column_paths_reference import ColumnPathsWorker +from .test_column_paths_protocol import args, attach, client, info, rows, seed +from .test_column_paths_protocol import backend as backend +from .typing_helpers import present + + +@pytest.mark.parametrize( + "value", + [ + ColumnMetadata(), + ColumnMetadata(comment="", tags={}), + ColumnMetadata(comment="snow 雪\x00", tags={"": "", "nul\x00": "雪"}), + ], +) +def test_helper_distinguishes_absence_and_empty_metadata(value: ColumnMetadata) -> None: + field = pa.field("a.b", pa.int32(), metadata={b"provider.opaque": b"\x00\xff"}) + updated = with_column_metadata(field, value) + assert column_metadata(updated) == value + assert updated.metadata is not None + assert updated.metadata[b"provider.opaque"] == b"\x00\xff" + assert (b"comment" in updated.metadata) == (value.comment is not None) + assert (b"tags" in updated.metadata) == (value.tags is not None) + assert with_column_metadata(updated, ColumnMetadata()).equals(field, check_metadata=True) + + +def test_comment_edit_preserves_existing_tag_encoding_and_unrelated_metadata() -> None: + field = pa.field("x", pa.int32(), metadata={b"tags": b' { "a" : "b" } ', b"other": b"kept"}) + updated = with_column_metadata(field, replace(column_metadata(field), comment="")) + assert updated.metadata == {b"comment": b"", b"tags": b' { "a" : "b" } ', b"other": b"kept"} + + +@pytest.mark.parametrize("key", [b"comment", b"tags", b"opaque"]) +def test_existing_duplicate_arrow_key_restriction_is_preserved(key: bytes) -> None: + # PyArrow supports duplicate metadata entries; its stubs only admit mappings. + metadata_factory = cast(Callable[[list[tuple[bytes, bytes]]], pa.KeyValueMetadata], pa.KeyValueMetadata) + field_factory = cast("Callable[[str, pa.DataType, bool, pa.KeyValueMetadata], pa.Field[Any]]", pa.field) + field = field_factory("x", pa.int32(), True, metadata_factory([(key, b"{}"), (key, b"{}")])) + with pytest.raises(ValueError, match="unique"): + column_metadata(field) + with pytest.raises(ValueError, match="unique"): + with_column_metadata(field, ColumnMetadata(comment="replacement")) + + +def test_mutated_python_tag_maps_are_revalidated_before_editing() -> None: + value = ColumnMetadata(tags={"a": "valid"}) + assert value.tags is not None + # Deliberately corrupt the validated mapping to exercise publication checks. + value.tags["a"] = cast(str, 1) + with pytest.raises(TypeError, match="string"): + with_column_metadata(pa.field("x", pa.int32()), value) + + +def test_recursive_validator_reaches_extension_storage() -> None: + class Wrapped(pa.ExtensionType): + """An opaque wrapper with an invalid metadata-bearing child.""" + + def __init__(self) -> None: + super().__init__(pa.struct([pa.field("x", pa.int32(), metadata={b"tags": b'{"a":1}'})]), "test.column_meta") + + def __arrow_ext_serialize__(self) -> bytes: + return b"" + + @classmethod + def __arrow_ext_deserialize__(cls, storage_type: pa.DataType, serialized: bytes) -> Wrapped: + return cls() + + with pytest.raises(ValueError, match="object of strings"): + validate_column_metadata(pa.schema([pa.field("wrapped", Wrapped())])) + + +def test_declarative_empty_comment_is_present_and_tags_are_preserved() -> None: + field = pa.field("x", pa.int32(), metadata={b"tags": b"{}", b"other": b"kept"}) + table = Table(name="t", columns=pa.schema([field]), column_comments={"x": ""}) + result = pa.ipc.read_schema(pa.py_buffer(table.to_table_info(["main"]).columns)).field("x") + assert result.metadata == {b"comment": b"", b"tags": b"{}", b"other": b"kept"} + + +def _columns(*, invalid: bool = False) -> SerializedSchema: + return SerializedSchema( + pa.schema([pa.field("x", pa.int32(), metadata={b"tags": b'{"a":1}'} if invalid else None)]) + .serialize() + .to_pybytes() + ) + + +def test_record_publication_revalidates_replaced_column_bytes() -> None: + records = [ + TableInfo(name="t", schema_path=["main"], columns=_columns(), constraints=[], comment=None, tags={}), + TableCreateRequest( + attach_opaque_data=b"attach", + schema_path=["main"], + name="t", + columns=_columns(), + constraint_definitions=[], + on_conflict=OnConflict.ERROR, + ), + ] + for record in records: + record.serialize_to_bytes() + object.__setattr__(record, "columns", _columns(invalid=True)) + with pytest.raises(ValueError, match="object of strings"): + record.serialize_to_bytes() + + +@pytest.mark.parametrize("operation", ["create", "add"]) +def test_bad_metadata_rejects_before_provider_mutation( + backend: ColumnPathsWorker, monkeypatch: pytest.MonkeyPatch, operation: str +) -> None: + with client() as c: + token = attach(c, None) + c.table_create(**args(token), columns=_columns()) + called = [] + monkeypatch.setattr( + backend._get_catalog(), + "table_" + operation if operation == "create" else "table_column_add", + lambda **kwargs: called.append(kwargs), + ) + with pytest.raises(ValueError, match="object of strings"): + if operation == "create": + request = TableCreateRequest( + **args(token), columns=_columns(), constraint_definitions=[], on_conflict=OnConflict.ERROR + ) + object.__setattr__(request, "columns", _columns(invalid=True)) + backend.catalog_table_create(request) + else: + backend.catalog_table_column_add(**args(token), column_path=["x"], column_definition=_columns(invalid=True)) + assert called == [] + + +@pytest.mark.parametrize("operation", ["get", "list", "contents"]) +def test_discovery_validates_even_previously_serialized_provider_records( + backend: ColumnPathsWorker, monkeypatch: pytest.MonkeyPatch, operation: str +) -> None: + with client() as c: + token = attach(c, None) + entry = TableInfo(name="t", schema_path=["main"], columns=_columns(), constraints=[], comment=None, tags={}) + TablesResponse.from_infos([entry]) + object.__setattr__(entry, "columns", _columns(invalid=True)) + catalog = backend._get_catalog() + monkeypatch.setattr(catalog, "table_get", lambda **kwargs: entry) + monkeypatch.setattr(catalog, "schema_contents", lambda **kwargs: [entry]) + monkeypatch.setattr( + catalog, + "catalog_contents", + lambda **kwargs: CatalogContentsResult( + schemas=[ + SchemaContentsInfo( + schema=SchemaInfo(attach_opaque_data=AttachOpaqueData(b""), path=["main"], comment=None, tags={}), + tables=[entry], + ) + ] + ), + ) + with pytest.raises(ValueError, match="object of strings"): + if operation == "get": + backend.catalog_table_get(token, ["main"], "t") + elif operation == "list": + backend.catalog_schema_contents_tables(token, ["main"]) + else: + backend.catalog_contents(token) + + +def test_signed_http_create_add_metadata_rollback_and_reconnect(backend: ColumnPathsWorker) -> None: + base = pa.schema( + [ + with_column_metadata( + pa.field("x", pa.int32(), metadata={b"origin": b"kept"}), ColumnMetadata(comment="", tags={}) + ), + pa.field( + "nested", + pa.struct( + [ + with_column_metadata( + pa.field("a.b", pa.string()), + ColumnMetadata(comment="snow 雪", tags={"nul\x00": "value\x00"}), + ) + ] + ), + ), + ], + metadata={b"schema": b"kept"}, + ) + added = with_column_metadata( + pa.field("added", pa.string(), metadata={b"origin": b"new"}), + ColumnMetadata(comment="\x00", tags={"team": "雪"}), + ) + with client() as c: + token = attach(c, None) + c.table_create(**args(token), columns=SerializedSchema(base.serialize().to_pybytes())) + seed(backend, token, [{"x": 1, "nested": {"a.b": "data"}}]) + assert info(c, token).equals(base, check_metadata=True) + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.table_column_add( + **args(token, tx), + column_path=["added"], + column_definition=SerializedSchema(pa.schema([added]).serialize().to_pybytes()), + ) + assert info(c, token, tx).field("added").equals(added, check_metadata=True) + assert "added" not in info(c, token).names + with client() as c: + c.catalog_transaction_rollback(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + assert info(c, token).equals(base, check_metadata=True) + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.table_column_add( + **args(token, tx), + column_path=["added"], + column_definition=SerializedSchema(pa.schema([added]).serialize().to_pybytes()), + ) + c.catalog_transaction_commit(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + with client() as c: + assert info(c, token).equals(base.append(added), check_metadata=True) + assert rows(backend, token) == [{"x": 1, "nested": {"a.b": "data"}, "added": None}] + provider = backend._get_catalog() + with pytest.raises(ValueError, match="object of strings"): + provider.table_column_add( + **args(backend._unwrap_attach(token)), + column_path=["bad"], + column_definition=pa.schema([pa.field("bad", pa.int32(), metadata={b"tags": b'{"a":1}'})]) + .serialize() + .to_pybytes(), + ) + assert info(c, token).equals(base.append(added), check_metadata=True) diff --git a/tests/catalog/test_column_paths_protocol.py b/tests/catalog/test_column_paths_protocol.py new file mode 100644 index 00000000..7cdd26d3 --- /dev/null +++ b/tests/catalog/test_column_paths_protocol.py @@ -0,0 +1,584 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Real RPC column-path edits, data rewrites, rollback, and capability guards.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path +from typing import Any, cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.catalog.capabilities import ( + COLUMN_NESTED_ADD, + COLUMN_NESTED_RENAME, + CONSTRAINT_ENFORCE, + CONSTRAINT_MUTATE, + CONSTRAINT_RECORDS_READ, + ROW_SCALAR_EXPRESSION, + TRANSACTION_DDL, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import AttachOpaqueData, SerializedSchema +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintEnforcement, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintValidation, +) +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.layout import LayoutChangeMode, NullOrder, SortDirection, SortKey, SqlExpression, TableLayoutChange +from vgi.catalog.sql import ColumnExpressions, column_expressions, with_column_expressions +from vgi.client import Client +from vgi.client.catalog_mixin import CatalogClientError +from vgi.column_path import column_definition, replace_column_field +from vgi.exceptions import CatalogCapabilityError +from vgi.meta_worker import MetaWorker +from vgi.protocol import CatalogAttachRequest, ClientCapabilities, VgiProtocol +from vgi.serve import create_app + +from .column_paths_reference import COLUMN_PROFILES, ColumnPathsCatalog, ColumnPathsWorker +from .typing_helpers import present + +KEY = b"column-path-reference-signing-key" +OFFER = ClientCapabilities( + engine="reference", native_formats=[], catalogs=[], can_stream=False, filter_encodings=[], catalog=COLUMN_PROFILES +) +SCHEMA = pa.schema( + [ + pa.field("id", pa.int32(), nullable=False), + pa.field("a.b", pa.int32(), metadata={b"origin": b"dotted"}), + pa.field( + "a", + pa.struct([pa.field("b", pa.int32(), metadata={b"origin": b"nested"}), pa.field("keep", pa.string())]), + metadata={b"parent": b"retained"}, + ), + ], + metadata={b"table": b"retained"}, +) +ROWS: list[dict[str, Any]] = [ + {"id": 1, "a.b": 7, "a": {"b": 2, "keep": "k"}}, + {"id": 2, "a.b": 8, "a": None}, + {"id": 3, "a.b": 9, "a": {"b": None, "keep": "z"}}, +] + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> ColumnPathsWorker: + """Isolate the durable reference behind both direct and HTTP workers.""" + monkeypatch.setattr(ColumnPathsCatalog, "database_path", str(tmp_path / "columns.sqlite"), raising=False) + worker = ColumnPathsWorker(quiet=True) + worker._signing_key = KEY + return worker + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(create_app(ColumnPathsWorker, describe=False, signing_key=KEY)) + ) + + +def attach(c: Client, offer: ClientCapabilities | None = OFFER) -> bytes: + return c.catalog_attach( + name="columns", data_version_spec=None, implementation_version=None, client_capabilities=offer + ).attach_opaque_data + + +def args(token: bytes, tx: bytes | None = None) -> dict[str, Any]: + return dict(attach_opaque_data=token, schema_path=["main"], name="t.with.dot", transaction_opaque_data=tx) + + +def field(name: str, typ: pa.DataType | None = None, **kwargs: Any) -> bytes: + return pa.schema([pa.field(name, typ or pa.int32(), **kwargs)]).serialize().to_pybytes() + + +def expression(sql: str, path: list[str] | None = None) -> SqlExpression: + return SqlExpression( + sql=sql, + profile=ROW_SCALAR_EXPRESSION, + schema_path=["main"] if path is None else path, + evaluation_context=QueryEvaluationContext(), + ) + + +def default_field(name: str, sql: str, **kwargs: Any) -> bytes: + return ( + pa.schema( + [with_column_expressions(pa.field(name, pa.int32(), **kwargs), ColumnExpressions(default=expression(sql)))] + ) + .serialize() + .to_pybytes() + ) + + +def info(c: Client, token: bytes, tx: bytes | None = None) -> pa.Schema: + return pa.ipc.read_schema(pa.py_buffer(present(c.table_get(**args(token, tx))).columns)) + + +def seed(worker: ColumnPathsWorker, token: bytes, rows: list[dict[str, Any]]) -> None: + worker._get_catalog().seed_rows(**args(worker._unwrap_attach(token)), rows=rows) + + +def rows(worker: ColumnPathsWorker, token: bytes, tx: bytes | None = None) -> list[dict[str, Any]]: + parts = worker._get_catalog().physical_rows( + **args(worker._unwrap_attach(token), worker._unwrap_transaction(tx, token) if tx is not None else None) + ) + return [row for part in parts for row in part["rows"]] + + +def setup(c: Client, backend: ColumnPathsWorker) -> bytes: + token = attach(c) + c.table_create( + **args(token), + columns=SerializedSchema(SCHEMA.serialize().to_pybytes()), + constraint_definitions=[ + ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[["id"]]).serialize_to_bytes() + ], + ) + seed(backend, token, ROWS) + return token + + +def test_nested_add_default_comment_and_nullability(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + c.table_column_add( + **args(token), + column_path=["a", "x.y"], + column_definition=SerializedSchema( + default_field("x.y", "9", metadata={b"comment": b"created", b"tags": b'{"team":"one"}'}) + ), + ) + assert [row["a"] for row in rows(backend, token)] == [ + {"b": 2, "keep": "k", "x.y": 9}, + None, + {"b": None, "keep": "z", "x.y": 9}, + ] + schema = info(c, token) + assert schema.metadata == SCHEMA.metadata and schema.field("a").metadata == {b"parent": b"retained"} + assert schema.field("a.b").metadata == {b"origin": b"dotted"} + c.table_column_default_set(**args(token), column_path=["a", "x.y"], expression=expression("3")) + c.table_not_null_set(**args(token), column_path=["a", "x.y"]) + c.table_column_comment_set(**args(token), column_path=["a", "x.y"], comment="changed") + seed(backend, token, [{"id": 4, "a.b": 10, "a": {}}]) + assert rows(backend, token)[-1]["a"]["x.y"] == 3 + assert rows(backend, token)[0]["a"]["x.y"] == 9 + c.table_column_default_drop(**args(token), column_path=["a", "x.y"]) + before = rows(backend, token) + with pytest.raises(ValueError, match="NOT NULL"): + seed(backend, token, [{"id": 5, "a": {}}]) + assert rows(backend, token) == before + c.table_not_null_drop(**args(token), column_path=["a", "x.y"]) + c.table_column_comment_set(**args(token), column_path=["a", "x.y"], comment=None) + definition = info(c, token).field("a").type.field("x.y") + assert definition.nullable and definition.metadata == {b"tags": b'{"team":"one"}'} + with client() as c: + assert info(c, token).field("a").type.field("x.y") == definition + + +def test_constraint_identity_and_revision_survive_rename_and_noop(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + original = c.table_get(**args(token)) + c.table_not_null_set(**args(token), column_path=["id"]) + unchanged = c.table_get(**args(token)) + assert present(unchanged).definition_revision == present(original).definition_revision + c.table_column_rename(**args(token), column_path=["id"], new_column_name="renamed.id") + renamed = c.table_get(**args(token)) + assert present(renamed).definition_revision != present(original).definition_revision + assert present(renamed).constraint_infos()[0].ref == present(original).constraint_infos()[0].ref + assert present(renamed).constraint_infos()[0].definition.column_paths == [["renamed.id"]] + + +def test_type_using_rename_and_drop_keep_literal_dotted_column_distinct(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + c.table_column_type_change( + **args(token), + column_path=["a", "b"], + column_definition=SerializedSchema(field("b", pa.float64())), + expression=expression('"a"."b" + 0.5'), + ) + assert rows(backend, token)[0]["a"]["b"] == 2.5 + assert rows(backend, token)[1]["a"] is None + c.table_column_rename(**args(token), column_path=["a", "b"], new_column_name="b.c") + schema = info(c, token) + assert schema.field("a").type.field("b.c").metadata == {b"origin": b"nested"} + assert schema.field("a.b").type == pa.int32() + assert rows(backend, token)[0]["a"]["b.c"] == 2.5 + c.table_column_drop(**args(token), column_path=["a", "b.c"]) + assert [row["a.b"] for row in rows(backend, token)] == [7, 8, 9] + assert rows(backend, token)[0]["a"] == {"keep": "k"} + + +def test_existing_nulls_and_failed_cast_do_not_change_data_or_version(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + original, schema, version = ( + rows(backend, token), + info(c, token), + c.catalog_version(attach_opaque_data=AttachOpaqueData(token)), + ) + with pytest.raises(ValueError, match="NOT NULL"): + backend.catalog_table_not_null_set(**args(token), column_path=["a", "b"]) + with pytest.raises((ValueError, pa.ArrowInvalid), match="range|convert|truncate"): + backend.catalog_table_column_type_change( + **args(token), + column_path=["a", "b"], + column_definition=field("b", pa.int8()), + expression=expression("1000").serialize_to_bytes(), + ) + assert rows(backend, token) == original and info(c, token).equals(schema, check_metadata=True) + assert c.catalog_version(attach_opaque_data=AttachOpaqueData(token)) == version + + +def test_metadata_edits_preserve_existing_future_write_partitions(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + c.table_alter_layout( + **args(token), + changes=TableLayoutChange( + mode=LayoutChangeMode.FUTURE_WRITES, + sorted_by=[ + SortKey( + expression=SqlExpression( + evaluation_context=QueryEvaluationContext(), + sql="id", + profile=ROW_SCALAR_EXPRESSION, + schema_path=["main"], + ), + direction=SortDirection.DESC, + null_order=NullOrder.LAST, + ).serialize_to_bytes() + ], + ), + ) + catalog_args = args(backend._unwrap_attach(token)) + original = backend._get_catalog().physical_rows(**catalog_args) + c.table_column_comment_set(**args(token), column_path=["a", "b"], comment="metadata only") + c.table_column_default_set(**args(token), column_path=["a", "b"], expression=expression("3")) + c.table_not_null_set(**args(token), column_path=["id"]) + assert backend._get_catalog().physical_rows(**catalog_args) == original + seed(backend, token, [{"id": 4}, {"id": 5}]) + assert [row["id"] for row in rows(backend, token)] == [1, 2, 3, 5, 4] + + +def test_transactional_column_edits_rollback_reconnect_and_conflict(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.table_column_rename(**args(token, tx), column_path=["a", "b"], new_column_name="changed") + c.table_column_add( + **args(token, tx), column_path=["new"], column_definition=SerializedSchema(default_field("new", "42")) + ) + assert rows(backend, token, tx)[0]["new"] == 42 + assert "new" not in rows(backend, token)[0] + with client() as c: + c.catalog_transaction_rollback(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + assert info(c, token).equals(SCHEMA, check_metadata=True) + first = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + second = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.table_column_rename(**args(token, first), column_path=["a.b"], new_column_name="renamed.literal") + c.table_column_drop(**args(token, second), column_path=["a", "b"]) + with client() as c: + c.catalog_transaction_commit(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(first)) + with pytest.raises(ValueError, match="conflict"): + backend.catalog_transaction_commit(token, present(second)) + assert rows(backend, token)[0]["renamed.literal"] == 7 + assert rows(backend, token)[0]["a"]["b"] == 2 + c.catalog_transaction_rollback( + attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(second) + ) + + +def test_capability_absence_downgrade_and_transaction_guarantee( + backend: ColumnPathsWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + with client() as c: + token = setup(c, backend) + unaware = attach(c, None) + with pytest.raises(CatalogCapabilityError, match=CONSTRAINT_RECORDS_READ): + backend.catalog_table_column_rename(**args(unaware), column_path=["id"], new_column_name="hidden") + old = attach( + c, + replace( + OFFER, + catalog=CatalogCapabilities( + read=[CONSTRAINT_RECORDS_READ], ddl=[CONSTRAINT_MUTATE], write=[CONSTRAINT_ENFORCE] + ), + ), + ) + with pytest.raises(CatalogCapabilityError, match=COLUMN_NESTED_ADD): + backend.catalog_table_column_add(**args(old), column_path=["a", "new"], column_definition=field("new")) + # Flat edits need no nested profile but must preserve canonical constraints. + c.table_column_rename(**args(old), column_path=["a.b"], new_column_name="literal") + limited = attach(c, replace(OFFER, catalog=replace(COLUMN_PROFILES, transaction=[]))) + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(limited)) + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + backend.catalog_table_column_rename(**args(limited, tx), column_path=["a", "b"], new_column_name="other") + c.catalog_transaction_rollback( + attach_opaque_data=AttachOpaqueData(limited), transaction_opaque_data=present(tx) + ) + monkeypatch.setattr( + backend._get_catalog(), + "profiles", + replace(COLUMN_PROFILES, ddl=[p for p in COLUMN_PROFILES.ddl if p != COLUMN_NESTED_RENAME]), + ) + with pytest.raises(CatalogCapabilityError, match=COLUMN_NESTED_RENAME): + backend.catalog_table_column_rename(**args(token), column_path=["a", "b"], new_column_name="other") + assert rows(backend, token)[0]["a"]["b"] == 2 + + +def test_missing_target_flags_and_parent_validation(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + version = c.catalog_version(attach_opaque_data=AttachOpaqueData(token)) + c.table_column_add( + **args(token), + column_path=["a", "b"], + column_definition=SerializedSchema(field("b")), + if_column_not_exists=True, + ) + c.table_column_drop(**args(token), column_path=["a", "absent"], if_column_exists=True) + assert c.catalog_version(attach_opaque_data=AttachOpaqueData(token)) == version + with pytest.raises(ValueError, match="non-struct"): + backend.catalog_table_column_add(**args(token), column_path=["a.b", "bad"], column_definition=field("bad")) + with pytest.raises(ValueError, match="already exists"): + backend.catalog_table_column_rename(**args(token), column_path=["a", "b"], new_column_name="keep") + assert c.catalog_version(attach_opaque_data=AttachOpaqueData(token)) == version + + +def test_meta_worker_routes_path_without_rendering_it(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + meta = MetaWorker([backend]) + token = meta.catalog_attach( + CatalogAttachRequest( + name="columns", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ) + ).attach_opaque_data + proxy = cast(VgiProtocol, meta) + proxy.catalog_table_column_comment_set(**args(token), column_path=["a", "b"], comment="nested only") + schema = pa.ipc.read_schema(pa.py_buffer(present(proxy.catalog_table_get(**args(token)).to_optional()).columns)) + assert b"comment" not in (schema.field("a.b").metadata or {}) + assert schema.field("a").type.field("b").metadata[b"comment"] == b"nested only" + + +def test_definition_validation_and_last_field_guard() -> None: + with pytest.raises(ValueError, match="match"): + column_definition(field("b"), ["a"]) + with pytest.raises(ValueError, match="trailing"): + column_definition(field("b") + b"trailing", ["b"]) + with pytest.raises(ValueError, match="only the target type"): + column_definition(field("b", metadata={b"default": b"4"}), ["b"], type_only=True) + with pytest.raises(ValueError, match="only the target type"): + column_definition(default_field("b", "4"), ["b"], type_only=True) + with pytest.raises(ValueError, match="last"): + replace_column_field(pa.schema([pa.field("only", pa.int32())]), ["only"], None) + with pytest.raises(ValueError, match="non-struct"): + replace_column_field(SCHEMA, ["a.b", "b"], None) + + +def test_mandatory_defaults_and_using_without_new_capabilities(backend: ColumnPathsWorker) -> None: + with client() as c: + token = attach(c, None) + c.table_create(**args(token), columns=SerializedSchema(default_field("temperature", "250"))) + seed(backend, token, [{}, {"temperature": None}, {"temperature": 310}]) + assert rows(backend, token) == [{"temperature": 250}, {"temperature": None}, {"temperature": 310}] + assert column_expressions(info(c, token).field("temperature")).default == expression("250") + c.table_column_default_set(**args(token), column_path=["temperature"], expression=expression("NULL")) + seed(backend, token, [{}]) + assert rows(backend, token)[-1] == {"temperature": None} + c.table_column_type_change( + **args(token), + column_path=["temperature"], + column_definition=SerializedSchema(field("temperature", pa.int64())), + ) + c.table_column_type_change( + **args(token), + column_path=["temperature"], + column_definition=SerializedSchema(field("temperature", pa.float64())), + expression=expression("temperature / 10.0"), + ) + assert rows(backend, token) == [ + {"temperature": 25.0}, + {"temperature": None}, + {"temperature": 31.0}, + {"temperature": None}, + ] + c.table_column_add( + **args(token), column_path=["added"], column_definition=SerializedSchema(default_field("added", "7")) + ) + assert all(row["added"] == 7 for row in rows(backend, token)) + before = c.table_get(**args(token)) + with pytest.raises(CatalogClientError, match="range|convert|truncate"): + c.table_column_type_change( + **args(token), + column_path=["temperature"], + column_definition=SerializedSchema(field("temperature", pa.int8())), + expression=expression("temperature * 1000"), + ) + assert c.table_get(**args(token)) == before + c.table_column_default_drop(**args(token), column_path=["temperature"]) + assert column_expressions(info(c, token).field("temperature")).default is None + with client() as c: + assert rows(backend, token)[0] == {"temperature": 25.0, "added": 7} + assert column_expressions(info(c, token).field("added")).default == expression("7") + + +def test_signed_http_expression_transaction_rollback_and_commit(backend: ColumnPathsWorker) -> None: + with client() as c: + token = attach(c) + c.table_create(**args(token), columns=SerializedSchema(default_field("temperature", "200"))) + seed(backend, token, [{"temperature": 123}]) + initial = c.table_get(**args(token)) + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.table_column_default_set(**args(token, tx), column_path=["temperature"], expression=expression("300")) + c.table_column_type_change( + **args(token, tx), + column_path=["temperature"], + column_definition=SerializedSchema(field("temperature", pa.float64())), + expression=expression("temperature / 10.0"), + ) + assert rows(backend, token, tx) == [{"temperature": 12.3}] + assert c.table_get(**args(token)) == initial + with client() as c: + c.catalog_transaction_rollback(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + assert c.table_get(**args(token)) == initial and rows(backend, token) == [{"temperature": 123}] + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.table_column_default_set(**args(token, tx), column_path=["temperature"], expression=expression("500")) + c.table_column_type_change( + **args(token, tx), + column_path=["temperature"], + column_definition=SerializedSchema(field("temperature", pa.float64())), + expression=expression("temperature / 10.0"), + ) + c.catalog_transaction_commit(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + with client() as c: + assert column_expressions(info(c, token).field("temperature")).default == expression("500") + seed(backend, token, [{}]) + assert rows(backend, token) == [{"temperature": 12.3}, {"temperature": 500.0}] + + +@pytest.mark.parametrize("operation", ["default", "using"]) +@pytest.mark.parametrize("failure", ["owner", "profile", "context", "script", "malformed", "trailing"]) +def test_expression_validation_precedes_effects(backend: ColumnPathsWorker, operation: str, failure: str) -> None: + with client() as c: + token = attach(c, None) + c.table_create(**args(token), columns=SerializedSchema(field("temperature"))) + seed(backend, token, [{"temperature": 123}]) + original, version = c.table_get(**args(token)), c.catalog_version(attach_opaque_data=AttachOpaqueData(token)) + value = expression("7") + if failure == "owner": + value = replace(value, schema_path=["other"]) + elif failure == "profile": + value = replace(value, profile="unsupported.profile.v1") + elif failure == "context": + value = replace(value, evaluation_context=QueryEvaluationContext(timezone="America/New_York")) + elif failure == "script": + value = replace(value, sql="7); DROP TABLE input_data; SELECT (8") + payload = value.serialize_to_bytes() + if failure == "malformed": + payload = b"not an IPC record" + elif failure == "trailing": + payload += b"trailing" + with pytest.raises((ValueError, pa.ArrowInvalid, OSError)): + if operation == "default": + backend.catalog_table_column_default_set(**args(token), column_path=["temperature"], expression=payload) + else: + backend.catalog_table_column_type_change( + **args(token), + column_path=["temperature"], + column_definition=field("temperature", pa.int64()), + expression=payload, + ) + assert ( + c.table_get(**args(token)) == original + and c.catalog_version(attach_opaque_data=AttachOpaqueData(token)) == version + ) + assert rows(backend, token) == [{"temperature": 123}] + + +def test_legacy_column_metadata_and_untyped_public_expressions_reject(backend: ColumnPathsWorker) -> None: + with client() as c: + token = attach(c, None) + with pytest.raises(CatalogClientError, match="canonical SqlExpression"): + c.table_create(**args(token), columns=SerializedSchema(field("temperature", metadata={b"default": b"7"}))) + c.table_create(**args(token), columns=SerializedSchema(field("temperature"))) + with pytest.raises(CatalogClientError, match="canonical SqlExpression"): + c.table_column_add( + **args(token), + column_path=["old"], + column_definition=SerializedSchema(field("old", metadata={b"default": b"7"})), + ) + for value in (None, "NULL"): + with pytest.raises(TypeError, match="SqlExpression"): + c.table_column_default_set( + **args(token), column_path=["temperature"], expression=cast(SqlExpression, value) + ) + with pytest.raises(TypeError, match="SqlExpression"): + c.table_column_type_change( + **args(token), + column_path=["temperature"], + column_definition=SerializedSchema(field("temperature", pa.int64())), + expression=cast(SqlExpression, "temperature + 1"), + ) + + +def test_native_not_null_request_uses_atomic_revision_and_transaction(backend: ColumnPathsWorker) -> None: + with client() as c: + token = setup(c, backend) + initial = c.table_get(**args(token)) + change = ConstraintChange( + operation=ConstraintOperation.ADD, + definition=ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[["a.b"]]), + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + ) + request = ConstraintsApplyRequest( + attach_opaque_data=token, + table=CatalogObjectRef(kind="table", schema_path=["main"], name="t.with.dot"), + expected_revision=present(present(initial).definition_revision), + changes=[change.serialize_to_bytes()], + ) + snapshot = c.table_constraints_apply(request) + assert present(snapshot).definition_revision != present(initial).definition_revision + assert not info(c, token).field("a.b").nullable + before, version = c.table_get(**args(token)), c.catalog_version(attach_opaque_data=AttachOpaqueData(token)) + with pytest.raises(CatalogClientError, match="stale"): + c.table_constraints_apply(request) + assert ( + c.table_get(**args(token)) == before + and c.catalog_version(attach_opaque_data=AttachOpaqueData(token)) == version + ) + bad = replace(change, definition=ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[["a", "b"]])) + with pytest.raises(CatalogClientError, match="NOT NULL"): + c.table_constraints_apply( + replace(request, expected_revision=snapshot.definition_revision, changes=[bad.serialize_to_bytes()]) + ) + assert c.table_get(**args(token)) == before and rows(backend, token) == ROWS + added = next(item for item in snapshot.constraint_infos() if item.definition.column_paths == [["a.b"]]) + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.table_constraints_apply( + replace( + request, + expected_revision=snapshot.definition_revision, + transaction_opaque_data=tx, + changes=[ + ConstraintChange( + operation=ConstraintOperation.DROP, ref=added.ref, cascade=False + ).serialize_to_bytes() + ], + ) + ) + assert info(c, token, tx).field("a.b").nullable + c.catalog_transaction_rollback(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + assert c.table_get(**args(token)) == before diff --git a/tests/catalog/test_constraint_contract.py b/tests/catalog/test_constraint_contract.py new file mode 100644 index 00000000..a166a107 --- /dev/null +++ b/tests/catalog/test_constraint_contract.py @@ -0,0 +1,507 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Constraint wire unions, exact identities and malformed IPC rejection.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import Any, cast + +import pyarrow as pa +import pytest + +from vgi.catalog import constraints as contract +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, + ConstraintValidation, + ForeignKeyDefinition, + ForeignKeyMatch, + ReferentialAction, +) +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot, ObjectDependency +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import SqlExpression + +from .typing_helpers import present + + +def table(name: str = "items", path: list[str] | None = None) -> CatalogObjectRef: + return CatalogObjectRef(kind="table", schema_path=path or ["a.b", 'quoted"schema'], name=name) + + +def identity(key: str = "logical-Key", owner: CatalogObjectRef | None = None) -> CatalogObjectRef: + owner = owner or table() + return CatalogObjectRef(kind="constraint", schema_path=owner.schema_path, owner_table=owner.name, name=key) + + +def definition( + kind: ConstraintKind = ConstraintKind.UNIQUE, paths: list[list[str]] | None = None, **kwargs: Any +) -> ConstraintDefinition: + return ConstraintDefinition(kind=kind, column_paths=paths if paths is not None else [["value"]], **kwargs) + + +def expression() -> SqlExpression: + return SqlExpression( + sql='"value" > 0', + profile="vgi.expression.duckdb.scalar.v1", + schema_path=table().schema_path, + evaluation_context=QueryEvaluationContext(timezone="UTC"), + ) + + +def info( + key: str = "logical-Key", + value: ConstraintDefinition | None = None, + owner: CatalogObjectRef | None = None, + **kwargs: Any, +) -> ConstraintInfo: + return ConstraintInfo( + ref=identity(key, owner), + definition=value or definition(), + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + **kwargs, + ) + + +def change(value: ConstraintDefinition | None = None, **kwargs: Any) -> ConstraintChange: + return ConstraintChange( + operation=ConstraintOperation.ADD, + definition=value or definition(), + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + **kwargs, + ) + + +def apply(changes: list[ConstraintChange] | None = None, **kwargs: Any) -> ConstraintsApplyRequest: + return ConstraintsApplyRequest( + attach_opaque_data=b"attachment", + table=table(), + expected_revision=b"revision-1", + changes=[item.serialize_to_bytes() for item in (changes if changes is not None else [change()])], + **kwargs, + ) + + +def snapshot(values: list[ConstraintInfo] | None = None) -> ConstraintSnapshot: + return ConstraintSnapshot( + table=table(), + definition_revision=b"revision-1", + constraints=[value.serialize_to_bytes() for value in (values if values is not None else [info()])], + ) + + +def batch( + record: ConstraintDefinition + | ForeignKeyDefinition + | ConstraintInfo + | ConstraintChange + | ConstraintsApplyRequest + | ConstraintSnapshot, +) -> pa.RecordBatch: + with pa.ipc.open_stream(record.serialize_to_bytes()) as reader: + return reader.read_next_batch() + + +def ipc(value: pa.RecordBatch, *, twice: bool = False) -> bytes: + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, value.schema) as writer: + writer.write_batch(value) + if twice: + writer.write_batch(value) + return sink.getvalue().to_pybytes() + + +def test_all_definition_kinds_roundtrip_with_typed_nested_records() -> None: + foreign = ForeignKeyDefinition( + referenced_table=table("parent"), + referenced_column_paths=[["id.part"], ["nested", "id"]], + match=ForeignKeyMatch.FULL, + on_update=ReferentialAction.CASCADE, + on_delete=ReferentialAction.SET_NULL, + ) + definitions = [ + definition(ConstraintKind.NOT_NULL), + definition(), + definition(ConstraintKind.PRIMARY_KEY, [["value"], ["other"]]), + definition(ConstraintKind.CHECK, [], check_expression=expression()), + definition(ConstraintKind.FOREIGN_KEY, [["child.part"], ["nested", "child"]], foreign_key=foreign), + ] + for original in definitions: + restored = ConstraintDefinition.deserialize_from_bytes(original.serialize_to_bytes()) + assert restored == original + assert ForeignKeyDefinition.deserialize_from_bytes(foreign.serialize_to_bytes()) == foreign + assert present(definitions[-1].foreign_key).referenced_column_paths == [["id.part"], ["nested", "id"]] + + +def test_raw_paths_keep_dots_quotes_case_and_struct_boundaries() -> None: + value = definition(paths=[["a.b"], ["a", "b"], ['quote"name'], ["Name"], ["name"]]) + schema = pa.schema( + [ + pa.field("a.b", pa.int32()), + pa.field("a", pa.struct([pa.field("b", pa.int32())])), + pa.field('quote"name', pa.int32()), + pa.field("Name", pa.int32()), + pa.field("name", pa.int32()), + ] + ) + value.validate_schema(schema) + restored = ConstraintDefinition.deserialize_from_bytes(value.serialize_to_bytes()) + assert restored.column_paths == value.column_paths + with pytest.raises(ValueError, match="missing or ambiguous"): + value.validate_schema(pa.schema([pa.field("a.b", pa.int32())])) + with pytest.raises(ValueError, match="non-struct"): + definition(paths=[["items", "element"]]).validate_schema(pa.schema([pa.field("items", pa.list_(pa.int32()))])) + + +@pytest.mark.parametrize( + "kwargs", + [ + {"kind": "UNKNOWN"}, + {"column_paths": []}, + {"column_paths": [["a"], ["a"]]}, + {"column_paths": [[""]]}, + {"column_paths": [["a\0b"]]}, + {"column_paths": ["a.b"]}, + {"column_paths": [["a"] * (contract.MAX_CONSTRAINT_PATH_DEPTH + 1)]}, + {"declared_name": ""}, + {"declared_name": "x" * (contract.MAX_CONSTRAINT_IDENTIFIER_BYTES + 1)}, + {"kind": ConstraintKind.NOT_NULL, "column_paths": [["a"], ["b"]]}, + {"kind": ConstraintKind.CHECK, "column_paths": []}, + {"kind": ConstraintKind.FOREIGN_KEY}, + ], +) +def test_definition_rejects_invalid_union_and_path_shapes(kwargs: dict[str, Any]) -> None: + with pytest.raises((TypeError, ValueError)): + replace(definition(), **kwargs) + + +def test_definition_rejects_payloads_on_wrong_kind_and_fk_arity() -> None: + foreign = ForeignKeyDefinition(referenced_table=table("parent"), referenced_column_paths=[["id"]]) + with pytest.raises(ValueError, match="only CHECK"): + replace(definition(), check_expression=expression()) + with pytest.raises(ValueError, match="only FOREIGN_KEY"): + replace(definition(), foreign_key=foreign) + with pytest.raises(ValueError, match="arity"): + definition(ConstraintKind.FOREIGN_KEY, [["a"], ["b"]], foreign_key=foreign) + for kwargs in [{"match": "PARTIAL"}, {"on_update": "UNKNOWN"}, {"on_delete": "UNKNOWN"}]: + with pytest.raises(ValueError): + replace(foreign, **cast(dict[str, Any], kwargs)) + with pytest.raises(ValueError, match="table reference"): + replace(foreign, referenced_table=identity()) + + +def test_info_stable_key_is_independent_of_declared_label_and_trust_flags() -> None: + original = info(value=definition(declared_name="visible SQL label")) + renamed = replace(original, definition=replace(original.definition, declared_name="new label")) + assert original.ref.key() == renamed.ref.key() + assert original.trusted + assert not replace(original, enforcement=ConstraintEnforcement.METADATA_ONLY).trusted + assert not replace(original, validation=ConstraintValidation.FUTURE_WRITES).trusted + assert not replace(original, validation=ConstraintValidation.UNVALIDATED).trusted + assert ConstraintInfo.deserialize_from_bytes(renamed.serialize_to_bytes()) == renamed + assert identity("Key").key() != identity("key").key() + assert identity(owner=table(path=["a.b"])).key() != identity(owner=table(path=["a", "b"])).key() + with pytest.raises(ValueError): + replace(original, enforcement=cast(ConstraintEnforcement, "unknown")) + with pytest.raises(ValueError): + replace(original, validation=cast(ConstraintValidation, "unknown")) + with pytest.raises(ValueError, match="constraint reference"): + replace(original, ref=table()) + + +def test_foreign_key_readback_requires_resolved_key_on_exact_target_table() -> None: + parent = table("parent") + foreign = definition( + ConstraintKind.FOREIGN_KEY, + foreign_key=ForeignKeyDefinition(referenced_table=parent, referenced_column_paths=[["id"]]), + ) + value = info(value=foreign, referenced_constraint=identity("parent-primary", parent)) + assert ConstraintInfo.deserialize_from_bytes(value.serialize_to_bytes()) == value + with pytest.raises(ValueError, match="requires its resolved"): + replace(value, referenced_constraint=None) + with pytest.raises(ValueError, match="another table"): + replace(value, referenced_constraint=identity("parent-primary", table("other"))) + with pytest.raises(ValueError, match="only foreign key"): + replace(info(), referenced_constraint=value.referenced_constraint) + self_target = replace(foreign, foreign_key=replace(present(foreign.foreign_key), referenced_table=table())) + with pytest.raises(ValueError, match="cannot reference itself"): + info(value=self_target, referenced_constraint=identity()) + + +def test_change_union_roundtrips_all_operations_and_explicit_false() -> None: + changes = [ + change(), + ConstraintChange(operation=ConstraintOperation.DROP, ref=identity(), cascade=False), + ConstraintChange(operation=ConstraintOperation.RENAME, ref=identity(), declared_name=None), + ConstraintChange(operation=ConstraintOperation.RENAME, ref=identity(), declared_name="new label"), + replace(change(), operation=ConstraintOperation.REPLACE, ref=identity()), + ] + for value in changes: + assert ConstraintChange.deserialize_from_bytes(value.serialize_to_bytes()) == value + assert changes[1].cascade is False + + +@pytest.mark.parametrize( + "operation,fields", + [ + ("unknown", {}), + (ConstraintOperation.ADD, {}), + (ConstraintOperation.DROP, {"ref": identity()}), + (ConstraintOperation.DROP, {"ref": identity(), "cascade": 1}), + (ConstraintOperation.DROP, {"ref": identity(), "cascade": False, "declared_name": "bad"}), + (ConstraintOperation.RENAME, {}), + (ConstraintOperation.RENAME, {"ref": identity(), "cascade": False}), + (ConstraintOperation.RENAME, {"ref": identity(), "definition": definition()}), + (ConstraintOperation.REPLACE, {"ref": identity()}), + ], +) +def test_change_union_rejects_missing_or_conflicting_fields( + operation: ConstraintOperation, fields: dict[str, Any] +) -> None: + with pytest.raises((TypeError, ValueError)): + ConstraintChange(operation=operation, **fields) + + +def test_add_replace_require_explicit_known_enforcement_and_validation() -> None: + for kwargs in [ + {"ref": identity()}, + {"enforcement": None}, + {"enforcement": "UNKNOWN"}, + {"validation": None}, + {"validation": "UNKNOWN"}, + {"cascade": False}, + {"declared_name": "outside-definition"}, + ]: + with pytest.raises((TypeError, ValueError)): + replace(change(), **cast(dict[str, Any], kwargs)) + + +def test_snapshot_and_request_roundtrip_and_enforce_exact_owner() -> None: + original = snapshot([info("Key"), info("key")]) + restored = ConstraintSnapshot.deserialize_from_bytes(original.serialize_to_bytes()) + assert restored == original + assert [item.ref.name for item in restored.constraint_infos()] == ["Key", "key"] + assert snapshot([]).constraint_infos() == [] + request = apply(transaction_opaque_data=b"transaction") + assert ConstraintsApplyRequest.deserialize_from_bytes(request.serialize_to_bytes()) == request + assert request.constraint_changes()[0].operation == ConstraintOperation.ADD + with pytest.raises(ValueError, match="another table"): + snapshot([info(owner=table("other"))]) + with pytest.raises(ValueError, match="another table"): + apply([ConstraintChange(operation=ConstraintOperation.DROP, ref=identity(owner=table("other")), cascade=False)]) + with pytest.raises(ValueError, match="owning table schema"): + wrong_expression = replace(expression(), schema_path=["other"]) + apply([change(definition(ConstraintKind.CHECK, [], check_expression=wrong_expression))]) + + +def test_snapshot_rejects_duplicate_keys_labels_and_primary_keys() -> None: + with pytest.raises(ValueError, match="duplicate constraint identity"): + snapshot([info(), info()]) + with pytest.raises(ValueError, match="duplicate declared"): + snapshot([info("one", definition(declared_name="same")), info("two", definition(declared_name="same"))]) + with pytest.raises(ValueError, match="multiple primary"): + snapshot( + [info("one", definition(ConstraintKind.PRIMARY_KEY)), info("two", definition(ConstraintKind.PRIMARY_KEY))] + ) + with pytest.raises(ValueError, match="key twice"): + apply( + [ + ConstraintChange(operation=ConstraintOperation.DROP, ref=identity(), cascade=False), + ConstraintChange(operation=ConstraintOperation.RENAME, ref=identity(), declared_name="new"), + ] + ) + + +@pytest.mark.parametrize("value", [b"", "not-bytes", b"x" * (contract.MAX_CONSTRAINT_TOKEN_BYTES + 1)]) +def test_revisions_and_owner_tokens_are_nonempty_bounded_bytes(value: bytes | str) -> None: + for kwargs in [{"attach_opaque_data": value}, {"expected_revision": value}, {"transaction_opaque_data": value}]: + with pytest.raises(ValueError): + replace(apply(), **cast(dict[str, Any], kwargs)) + with pytest.raises(ValueError): + replace(snapshot(), definition_revision=cast(bytes, value)) + + +def test_empty_or_malformed_nested_records_reject_before_mutation() -> None: + with pytest.raises(ValueError): + apply([]) + for values in [[b""], [b"not ipc"], [None]]: + with pytest.raises((TypeError, ValueError, pa.ArrowInvalid)): + replace(snapshot(), constraints=cast(list[bytes], values)) + with pytest.raises((TypeError, ValueError, pa.ArrowInvalid)): + replace(apply(), changes=cast(list[bytes], values)) + with pytest.raises(ValueError, match="exact schema"): + replace(snapshot(), constraints=[definition().serialize_to_bytes()]) + + +def test_record_decoder_rejects_extra_rows_batches_trailing_bytes_and_unknown_fields() -> None: + original = definition() + value = batch(original) + wrong_batches = [ + pa.RecordBatch.from_arrays( + [pa.concat_arrays([column, column]) for column in value.columns], schema=value.schema + ), + value.select(list(range(value.num_columns - 1))), + value.append_column("unknown", pa.array([1])), + value.replace_schema_metadata({b"unexpected": b"metadata"}), + ] + for wrong in wrong_batches: + with pytest.raises(ValueError, match="one row|exact schema"): + ConstraintDefinition.deserialize_from_bytes(ipc(wrong)) + for wrong_bytes in [ipc(value, twice=True), original.serialize_to_bytes() + b"trailing"]: + with pytest.raises(ValueError): + ConstraintDefinition.deserialize_from_bytes(wrong_bytes) + + +def test_decoder_rejects_wrong_nullability_and_null_required_fields() -> None: + value = batch(definition()) + fields = list(value.schema) + fields[0] = fields[0].with_nullable(True) + wrong = pa.RecordBatch.from_arrays(value.columns, schema=pa.schema(fields)) + with pytest.raises(ValueError, match="exact schema"): + ConstraintDefinition.deserialize_from_bytes(ipc(wrong)) + columns = list(value.columns) + columns[0] = pa.array([None], type=value.schema.field(0).type) + wrong = pa.RecordBatch.from_arrays(columns, schema=value.schema) + with pytest.raises(ValueError, match="non-nullable"): + ConstraintDefinition.deserialize_from_bytes(ipc(wrong)) + + +def test_unknown_wire_enum_is_not_silently_defaulted() -> None: + value = batch(definition()) + wrong = value.set_column(0, value.schema.field(0), pa.array(["UNKNOWN"], type=value.schema.field(0).type)) + with pytest.raises(ValueError): + ConstraintDefinition.deserialize_from_bytes(ipc(wrong)) + + +def test_mutated_nested_lists_and_oversized_envelopes_revalidate_on_serialization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + value = definition() + value.column_paths.append(["value"]) + with pytest.raises(ValueError, match="unique"): + value.serialize_to_bytes() + request = apply() + request.changes.append(b"not IPC") + with pytest.raises((ValueError, pa.ArrowInvalid)): + request.serialize_to_bytes() + valid = definition() + encoded = valid.serialize_to_bytes() + monkeypatch.setattr(contract, "MAX_CONSTRAINT_BYTES", len(encoded) - 1) + with pytest.raises(ValueError, match="size"): + ConstraintDefinition.deserialize_from_bytes(encoded) + with pytest.raises(ValueError, match="envelope"): + valid.serialize_to_bytes() + + +def test_constraint_refs_join_existing_owned_backing_index_dependency_closure() -> None: + owner, constraint = table(), identity() + index = CatalogObjectRef(kind="index", schema_path=owner.schema_path, name="backing-index") + + def edge(dependent: CatalogObjectRef, subject: CatalogObjectRef, kind: str) -> bytes: + return ObjectDependency( + dependent=dependent.serialize_to_bytes(), + subject=subject.serialize_to_bytes(), + kind=kind, + blocks_alter=True, + ).serialize_to_bytes() + + graph = DependencySnapshot( + catalog_version=3, + objects=[value.serialize_to_bytes() for value in [owner, constraint, index]], + dependencies=[ + edge(constraint, owner, "owned"), + edge(index, constraint, "owned"), + edge(constraint, index, "requires"), + ], + ) + assert {value.key() for value in graph.drop_set(constraint)} == {constraint.key(), index.key()} + with pytest.raises(ValueError, match="dependent objects"): + graph.drop_set(index) + with pytest.raises(ValueError): + replace(constraint, owner_table=None) + with pytest.raises(ValueError): + replace(owner, owner_table="unexpected") + + +def test_constraint_fk_paths_and_mixed_backing_index_cycles_preserve_exact_drop_closure() -> None: + parent, child = table("parent"), table("child") + key, foreign = identity("primary-key", parent), identity("foreign-key", child) + backing = CatalogObjectRef(kind="index", schema_path=parent.schema_path, name="backing-index") + + def edge( + dependent: CatalogObjectRef, + subject: CatalogObjectRef, + kind: str = "requires", + paths: list[list[str]] | None = None, + blocks: bool = True, + ) -> bytes: + return ObjectDependency( + dependent=dependent.serialize_to_bytes(), + subject=subject.serialize_to_bytes(), + kind=kind, + blocks_alter=blocks, + subject_column_paths=paths, + ).serialize_to_bytes() + + graph = DependencySnapshot( + catalog_version=7, + objects=[value.serialize_to_bytes() for value in [parent, child, key, foreign, backing]], + dependencies=[ + edge(key, parent, "owned", [["id.part"], ["nested", "id"]]), + edge(backing, key, "owned", blocks=False), + edge(key, backing), + edge(foreign, child, "owned", [["parent_id"], ["nested", "parent_id"]]), + edge(foreign, key), + edge(foreign, parent, paths=[["id.part"], ["nested", "id"]]), + ], + ) + schemas = { + parent.key(): pa.schema( + [ + pa.field("id.part", pa.int64()), + pa.field("nested", pa.struct([pa.field("id", pa.int64())])), + pa.field("id", pa.struct([pa.field("part", pa.int64())])), + ] + ), + child.key(): pa.schema( + [pa.field("parent_id", pa.int64()), pa.field("nested", pa.struct([pa.field("parent_id", pa.int64())]))] + ), + } + graph.validate_columns(schemas) + graph.assert_alter_allowed(parent, column_path=["id", "part"], action="type", schemas=schemas) + for path in [["id.part"], ["nested"], ["nested", "id"]]: + with pytest.raises(ValueError, match="dependent"): + graph.assert_alter_allowed(parent, column_path=path, action="type", schemas=schemas) + with pytest.raises(ValueError, match="dependent"): + graph.plan_alter(parent, column_path=path, action="drop", schemas=schemas) + plan = graph.plan_alter(parent, column_path=path, action="drop", cascade=True, schemas=schemas) + assert {value.key() for value in plan.drop_objects} == {key.key(), foreign.key(), backing.key()} + assert [(drop.table, drop.column_path) for drop in plan.drop_columns] == [(parent, path)] + with pytest.raises(ValueError, match="dependent"): + graph.drop_set(backing) + assert {value.key() for value in graph.drop_set(backing, cascade=True)} == { + backing.key(), + key.key(), + foreign.key(), + } + assert {value.key() for value in graph.drop_set(parent, cascade=True)} == { + parent.key(), + key.key(), + foreign.key(), + backing.key(), + } + # The FK protects exactly the key columns, not every column of its parent. + foreign_edge = next( + edge for edge in graph.edges() if edge.dependent_ref() == foreign and edge.subject_ref() == parent + ) + assert foreign_edge.subject_column_paths == [["id.part"], ["nested", "id"]] diff --git a/tests/catalog/test_constraint_example_cross_sdk.py b/tests/catalog/test_constraint_example_cross_sdk.py new file mode 100644 index 00000000..b0f189ef --- /dev/null +++ b/tests/catalog/test_constraint_example_cross_sdk.py @@ -0,0 +1,42 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Exercise canonical discovery through independently built example workers.""" + +from __future__ import annotations + +import json +import os + +import pyarrow as pa +import pytest + +from vgi.catalog import SchemaObjectType +from vgi.catalog.constraints import ConstraintEnforcement, ConstraintValidation, validate_constraint_schema +from vgi.client import Client + +WORKERS: dict[str, list[str]] = json.loads(os.environ.get("VGI_METADATA_WORKERS", "{}")) + + +@pytest.mark.parametrize("sdk", sorted(WORKERS)) +def test_example_constraints_are_explicit_stable_untrusted_records(sdk: str) -> None: + with Client(server_path=WORKERS[sdk], worker_limit=1) as client: + token = client.catalog_attach( + name="example", data_version_spec=None, implementation_version=None + ).attach_opaque_data + tables = client.schema_contents(attach_opaque_data=token, path=["data"], type=SchemaObjectType.TABLE) + by_name = {table.name: table for table in tables} + for name in ("departments", "products", "employees", "projects"): + listed = by_name[name] + fetched = client.table_get(attach_opaque_data=token, schema_path=["data"], name=name) + assert fetched is not None + assert fetched.constraints == listed.constraints + infos = fetched.constraint_infos() + assert infos, (sdk, name) + assert all(item.enforcement == ConstraintEnforcement.METADATA_ONLY for item in infos) + assert all(item.validation == ConstraintValidation.UNVALIDATED for item in infos) + assert not any(item.trusted for item in infos) + validate_constraint_schema( + pa.ipc.read_schema(pa.py_buffer(fetched.columns)), + [item.definition for item in infos], + trusted=[], + ) diff --git a/tests/catalog/test_constraint_fixture_boundaries.py b/tests/catalog/test_constraint_fixture_boundaries.py new file mode 100644 index 00000000..63cc94ea --- /dev/null +++ b/tests/catalog/test_constraint_fixture_boundaries.py @@ -0,0 +1,28 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Writable reconciliation facts require actual value checks before storage.""" + +from __future__ import annotations + +from datetime import UTC, datetime +from pathlib import Path + +import pytest + +from vgi._test_fixtures.schema_reconcile.worker import TABLES, _all_rows, _insert_row, _update_row + + +def test_reconciliation_storage_rejects_nested_null_without_publishing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """All write entry points enforce required values, including nested leaves.""" + monkeypatch.setenv("VGI_SCHEMA_RECONCILE_DB", str(tmp_path / "reconcile.sqlite")) + spec = TABLES["demo"] + payload = {"id": 1, "ts": datetime(2026, 1, 1, tzinfo=UTC), "nested": {"a": 2}, "tags": []} + _insert_row(spec, 1, payload) + original = _all_rows(spec) + with pytest.raises(ValueError, match="NULL|null"): + _insert_row(spec, 2, {**payload, "nested": {"a": None}}) + with pytest.raises(ValueError, match="NULL|null"): + _update_row(spec, 1, {"nested": {"a": None}}) + assert _all_rows(spec) == original diff --git a/tests/catalog/test_constraint_public_contract.py b/tests/catalog/test_constraint_public_contract.py new file mode 100644 index 00000000..be0e78fa --- /dev/null +++ b/tests/catalog/test_constraint_public_contract.py @@ -0,0 +1,268 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Canonical public carriers and fail-closed worker capability boundaries.""" + +from __future__ import annotations + +from dataclasses import replace +from types import SimpleNamespace +from typing import Any + +import pyarrow as pa +import pytest + +from vgi.catalog.capabilities import ( + CONSTRAINT_CREATE, + CONSTRAINT_ENFORCE, + CONSTRAINT_MUTATE, + CONSTRAINT_RECORDS_READ, + TRANSACTION_DDL, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import OnConflict, SerializedSchema, TableInfo +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, + ConstraintValidation, +) +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.exceptions import CatalogCapabilityError +from vgi.logical_types import variant_field +from vgi.meta_worker import MetaWorker +from vgi.protocol import TableCreateRequest +from vgi.worker import Worker + +TABLE = CatalogObjectRef(kind="table", schema_path=["a.b", "nested"], name="items") +DEFINITION = ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[["a.b"]]) +INFO = ConstraintInfo( + ref=CatalogObjectRef(kind="constraint", schema_path=TABLE.schema_path, owner_table=TABLE.name, name="key-1"), + definition=DEFINITION, + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, +) +SCHEMA = pa.schema([pa.field("a.b", pa.int64(), nullable=False)]) +PROFILES = CatalogCapabilities( + read=[CONSTRAINT_RECORDS_READ], + ddl=sorted([CONSTRAINT_CREATE, CONSTRAINT_MUTATE]), + write=[CONSTRAINT_ENFORCE], + transaction=[TRANSACTION_DDL], +) + + +def table_info(**changes: Any) -> TableInfo: + return replace( + TableInfo( + name=TABLE.name, + schema_path=TABLE.schema_path, + comment=None, + tags={}, + columns=SerializedSchema(SCHEMA.serialize().to_pybytes()), + constraints=[INFO.serialize_to_bytes()], + definition_revision=b"revision-1", + insert_function=b"opaque-write-descriptor", + write_result_modes={"insert": "count"}, + ), + **changes, + ) + + +def create_request(**changes: Any) -> TableCreateRequest: + return replace( + TableCreateRequest( + attach_opaque_data=b"sealed-attach", + schema_path=TABLE.schema_path, + name=TABLE.name, + columns=SCHEMA.serialize().to_pybytes(), + on_conflict=OnConflict.ERROR, + constraint_definitions=[DEFINITION.serialize_to_bytes()], + ), + **changes, + ) + + +def apply_request(**changes: Any) -> ConstraintsApplyRequest: + return replace( + ConstraintsApplyRequest( + attach_opaque_data=b"sealed-attach", + table=TABLE, + expected_revision=b"revision-1", + changes=[ + ConstraintChange( + operation=ConstraintOperation.RENAME, ref=INFO.ref, declared_name="renamed" + ).serialize_to_bytes() + ], + ), + **changes, + ) + + +def worker( + monkeypatch: pytest.MonkeyPatch, selected: CatalogCapabilities = PROFILES, available: CatalogCapabilities = PROFILES +) -> tuple[Worker, SimpleNamespace, list[Any]]: + instance = object.__new__(Worker) + calls = [] + provider = SimpleNamespace( + catalog_capabilities=lambda **kwargs: available, + table_create=lambda **kwargs: calls.append(kwargs), + table_get=lambda **kwargs: table_info(), + table_insert_function_get=lambda **kwargs: calls.append(kwargs), + ) + + def apply(request: ConstraintsApplyRequest) -> ConstraintSnapshot: + calls.append(request) + return ConstraintSnapshot( + table=TABLE, definition_revision=b"revision-2", constraints=[INFO.serialize_to_bytes()] + ) + + provider.table_constraints_apply = apply + monkeypatch.setattr(instance, "_get_catalog", lambda: provider) + monkeypatch.setattr(instance, "_catalog_agreement", lambda envelope: selected) + monkeypatch.setattr(instance, "_unwrap_attach", lambda envelope: b"provider-attach") + monkeypatch.setattr(instance, "_unwrap_tx_opt", lambda tx, attach: None if tx is None else b"provider-tx") + monkeypatch.setattr(instance, "_enrich_catalog_span", lambda **kwargs: None) + return instance, provider, calls + + +def test_public_carriers_have_one_required_authoritative_list() -> None: + legacy = { + "not_null_constraints", + "unique_constraints", + "check_constraints", + "primary_key_constraints", + "foreign_key_constraints", + } + assert not legacy.intersection(TableInfo.ARROW_SCHEMA.names) + assert not legacy.intersection(TableCreateRequest.ARROW_SCHEMA.names) + assert not TableInfo.ARROW_SCHEMA.field("constraints").nullable + assert not TableCreateRequest.ARROW_SCHEMA.field("constraint_definitions").nullable + assert TableInfo.ARROW_SCHEMA.field("definition_revision").nullable + assert TableInfo.deserialize_from_bytes(table_info().serialize_to_bytes()) == table_info() + assert TableCreateRequest.deserialize_from_bytes(create_request().serialize_to_bytes()) == create_request() + with pytest.raises(ValueError, match="bounded list"): + table_info(constraints=None) + with pytest.raises(ValueError, match="bounded list"): + create_request(constraint_definitions=None) + + +def test_profiles_require_readback_and_create_requires_enforcement() -> None: + assert PROFILES.select(None) == CatalogCapabilities() + assert PROFILES.select(CatalogCapabilities(ddl=[CONSTRAINT_CREATE, CONSTRAINT_MUTATE])) == CatalogCapabilities() + read_and_create = CatalogCapabilities(read=[CONSTRAINT_RECORDS_READ], ddl=[CONSTRAINT_CREATE]) + assert PROFILES.select(read_and_create).ddl == [] + + +def test_discovery_never_hides_constraints_and_mutation_requires_revision() -> None: + with pytest.raises(CatalogCapabilityError): + Worker._table_for_capabilities(table_info(), CatalogCapabilities()) + read_only = CatalogCapabilities(read=[CONSTRAINT_RECORDS_READ]) + result = Worker._table_for_capabilities(table_info(), read_only) + assert result.constraints == table_info().constraints + assert result.insert_function is None and result.write_result_modes == {} + assert Worker._table_for_capabilities(table_info(), PROFILES).insert_function is not None + with pytest.raises(ValueError, match="definition revision"): + Worker._table_for_capabilities(table_info(definition_revision=None), PROFILES) + + +@pytest.mark.parametrize("trusted", [False, True]) +def test_discovery_rejects_nullability_that_does_not_match_trust(trusted: bool) -> None: + info = INFO if trusted else replace(INFO, enforcement=ConstraintEnforcement.METADATA_ONLY) + wrong = pa.schema([pa.field("a.b", pa.int64(), nullable=trusted)]) + with pytest.raises(ValueError, match="nullability"): + Worker._table_for_capabilities( + table_info(columns=wrong.serialize().to_pybytes(), constraints=[info.serialize_to_bytes()]), PROFILES + ) + + +def test_create_checks_negotiation_and_current_support_before_provider(monkeypatch: pytest.MonkeyPatch) -> None: + for selected, available in [(CatalogCapabilities(), PROFILES), (PROFILES, CatalogCapabilities())]: + instance, _, calls = worker(monkeypatch, selected, available) + with pytest.raises(CatalogCapabilityError): + instance.catalog_table_create(create_request()) + assert calls == [] + instance, _, calls = worker(monkeypatch) + instance.catalog_table_create(create_request()) + assert calls[0]["constraint_definitions"] == create_request().constraint_definitions + assert calls[0]["attach_opaque_data"] == b"provider-attach" + + +def test_apply_unwraps_context_once_and_rejects_transaction_without_guarantee(monkeypatch: pytest.MonkeyPatch) -> None: + instance, _, calls = worker(monkeypatch) + request = apply_request(transaction_opaque_data=b"sealed-tx") + result = instance.catalog_table_constraints_apply(request) + assert result.definition_revision == b"revision-2" + assert len(calls) == 1 + assert calls[0].attach_opaque_data == b"provider-attach" + assert calls[0].transaction_opaque_data == b"provider-tx" + assert calls[0].expected_revision == request.expected_revision + instance, _, calls = worker(monkeypatch, replace(PROFILES, transaction=[])) + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + instance.catalog_table_constraints_apply(request) + assert calls == [] + + +def test_unqualified_write_discovery_and_wrong_mutation_owner_reject(monkeypatch: pytest.MonkeyPatch) -> None: + instance, _, calls = worker(monkeypatch, CatalogCapabilities(read=[CONSTRAINT_RECORDS_READ])) + with pytest.raises(CatalogCapabilityError, match=CONSTRAINT_ENFORCE): + instance.catalog_table_insert_function_get(b"sealed-attach", TABLE.schema_path, TABLE.name) + assert calls == [] + instance, provider, _ = worker(monkeypatch) + provider.table_constraints_apply = lambda request: ConstraintSnapshot( + table=replace(TABLE, name="other"), definition_revision=b"revision-2", constraints=[] + ) + with pytest.raises(ValueError, match="different table"): + instance.catalog_table_constraints_apply(apply_request()) + + +def test_legacy_replacement_and_not_null_cannot_bypass_canonical_mutation(monkeypatch: pytest.MonkeyPatch) -> None: + instance, _, calls = worker(monkeypatch, CatalogCapabilities(read=[CONSTRAINT_RECORDS_READ])) + with pytest.raises(CatalogCapabilityError, match=CONSTRAINT_MUTATE): + instance.catalog_table_create(create_request(on_conflict=OnConflict.REPLACE, constraint_definitions=[])) + with pytest.raises(CatalogCapabilityError, match=CONSTRAINT_MUTATE): + instance.catalog_table_not_null_set(b"sealed-attach", TABLE.schema_path, TABLE.name, ["a.b"]) + assert calls == [] + + +def test_meta_worker_routes_canonical_apply_without_rewriting_attachment(monkeypatch: pytest.MonkeyPatch) -> None: + instance = object.__new__(MetaWorker) + request = apply_request() + expected = ConstraintSnapshot(table=TABLE, definition_revision=b"revision-2", constraints=[]) + seen = [] + + def collect(value: ConstraintsApplyRequest) -> ConstraintSnapshot: + seen.append(value) + return expected + + child = SimpleNamespace(catalog_table_constraints_apply=collect) + + def route(envelope: bytes, *, method_name: str) -> SimpleNamespace: + assert envelope == request.attach_opaque_data + assert method_name == "catalog_table_constraints_apply" + return child + + monkeypatch.setattr(instance, "_worker_for_attach", route) + assert instance.catalog_table_constraints_apply(request) == expected + assert seen == [request] + + +def test_storage_nullability_is_not_a_synthetic_sql_constraint() -> None: + from vgi.catalog.constraints import validate_constraint_schema + + schema = pa.schema( + [ + pa.field("rowid", pa.int64(), nullable=False, metadata={b"is_row_id": b""}), + variant_field("variant"), + ] + ) + validate_constraint_schema(schema, []) + for path in [["rowid"], ["variant", "metadata"]]: + with pytest.raises(ValueError, match="storage"): + validate_constraint_schema( + schema, [ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[path])] + ) diff --git a/tests/catalog/test_constraints_public_lifecycle.py b/tests/catalog/test_constraints_public_lifecycle.py new file mode 100644 index 00000000..0dc2990c --- /dev/null +++ b/tests/catalog/test_constraints_public_lifecycle.py @@ -0,0 +1,210 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Durable constraint identities and atomic revisions over the public HTTP RPC.""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import replace +from pathlib import Path +from typing import Any, cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.catalog import TableInfo +from vgi.catalog.catalog_interface import AttachOpaqueData, SerializedSchema, TransactionOpaqueData +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, +) +from vgi.client import Client +from vgi.client.catalog_mixin import CatalogClientError +from vgi.protocol import ClientCapabilities +from vgi.serve import create_app + +from .constraints_protocol_reference import PROFILES, ConstraintProvider, ConstraintWorker +from .constraints_reference import ConstraintCatalog +from .test_constraints_reference import add, check, definition, foreign_key, table +from .typing_helpers import present + +KEY = b"constraint-reference-signing-key" +OFFER = ClientCapabilities( + engine="reference", native_formats=[], catalogs=[], can_stream=False, filter_encodings=[], catalog=PROFILES +) +COLUMNS = pa.schema(cast("list[pa.Field[Any]]", [pa.field("id", pa.int64()), pa.field("label", pa.string())])) + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> ConstraintProvider: + monkeypatch.setattr(ConstraintProvider, "database_path", str(tmp_path / "constraints.sqlite"), raising=False) + return ConstraintProvider() + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(create_app(ConstraintWorker, describe=False, signing_key=KEY)) + ) + + +def attach(c: Client) -> AttachOpaqueData: + return c.catalog_attach( + name="constraints", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ).attach_opaque_data + + +def create( + c: Client, + token: AttachOpaqueData, + name: str = "items", + definitions: Sequence[ConstraintDefinition] = (), + columns: pa.Schema = COLUMNS, + tx: TransactionOpaqueData | None = None, +) -> None: + c.table_create( + attach_opaque_data=token, + schema_path=["main"], + name=name, + columns=SerializedSchema(columns.serialize().to_pybytes()), + constraint_definitions=[item.serialize_to_bytes() for item in definitions], + transaction_opaque_data=tx, + ) + + +def get(c: Client, token: AttachOpaqueData, name: str = "items", tx: TransactionOpaqueData | None = None) -> TableInfo: + return present(c.table_get(attach_opaque_data=token, schema_path=["main"], name=name, transaction_opaque_data=tx)) + + +def apply( + c: Client, + token: AttachOpaqueData, + before: TableInfo, + *changes: ConstraintChange, + tx: TransactionOpaqueData | None = None, +) -> ConstraintSnapshot: + return c.table_constraints_apply( + ConstraintsApplyRequest( + attach_opaque_data=token, + table=table(before.name), + expected_revision=present(before.definition_revision), + changes=[change.serialize_to_bytes() for change in changes], + transaction_opaque_data=tx, + ) + ) + + +def test_create_discovery_mutation_restart_and_stale_revision(backend: ConstraintProvider) -> None: + with client() as c: + token = attach(c) + create(c, token, definitions=[definition(ConstraintKind.UNIQUE, "id", name="first"), check("id > 0")]) + original = get(c, token) + infos = original.constraint_infos() + assert len(infos) == 2 and all(info.trusted for info in infos) + contents = c.contents(attach_opaque_data=token) + assert contents.schemas[0].tables[0] == original + changed = apply( + c, + token, + original, + ConstraintChange(operation=ConstraintOperation.RENAME, ref=infos[0].ref, declared_name="second"), + add(definition(ConstraintKind.NOT_NULL, "label")), + ) + assert changed.definition_revision != original.definition_revision + assert changed.constraint_infos()[0].ref == infos[0].ref + current = get(c, token) + assert current.constraints == changed.constraints + assert not pa.ipc.read_schema(pa.py_buffer(current.columns)).field("label").nullable + with pytest.raises(CatalogClientError, match="revision"): + apply( + c, + token, + original, + ConstraintChange(operation=ConstraintOperation.DROP, ref=infos[0].ref, cascade=False), + ) + assert get(c, token) == current + with client() as c: + assert get(c, token) == current + replacement = replace(changed.constraint_infos()[0].definition, column_paths=[["label"]]) + newer = apply( + c, + token, + current, + ConstraintChange( + operation=ConstraintOperation.REPLACE, + ref=infos[0].ref, + definition=replacement, + enforcement=infos[0].enforcement, + validation=infos[0].validation, + ), + ) + assert newer.constraint_infos()[0].ref == infos[0].ref + assert newer.definition_revision != current.definition_revision + + +def test_transaction_visibility_rollback_commit_and_attachment_scope(backend: ConstraintProvider) -> None: + with client() as c, client() as other: + token, another = attach(c), attach(other) + create(c, token) + original = get(c, token) + tx = c.catalog_transaction_begin(attach_opaque_data=token) + pending = apply(c, token, original, add(definition(ConstraintKind.NOT_NULL, "id")), tx=tx) + assert get(c, token) == original + assert get(c, token, tx=tx).definition_revision == pending.definition_revision + with pytest.raises(CatalogClientError): + apply(other, another, original, add(definition(ConstraintKind.NOT_NULL, "id")), tx=tx) + with client() as c: + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert get(c, token) == original + tx = c.catalog_transaction_begin(attach_opaque_data=token) + pending = apply(c, token, original, add(definition(ConstraintKind.UNIQUE, "id")), tx=tx) + with client() as c: + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert get(c, token).definition_revision == pending.definition_revision + + +def test_foreign_key_identity_restrict_and_cascade_are_atomic(backend: ConstraintProvider) -> None: + with client() as c: + token = attach(c) + create(c, token, "parent", [definition(ConstraintKind.UNIQUE, "id")]) + create(c, token, "child", [foreign_key(table("parent"), ["id"], ["id"])]) + parent, child = get(c, token, "parent"), get(c, token, "child") + key = parent.constraint_infos()[0].ref + assert child.constraint_infos()[0].referenced_constraint == key + with pytest.raises(CatalogClientError, match="depend|referenc"): + apply(c, token, parent, ConstraintChange(operation=ConstraintOperation.DROP, ref=key, cascade=False)) + assert get(c, token, "parent") == parent and get(c, token, "child") == child + apply(c, token, parent, ConstraintChange(operation=ConstraintOperation.DROP, ref=key, cascade=True)) + assert get(c, token, "parent").constraints == [] + assert get(c, token, "child").constraints == [] + assert get(c, token, "child").definition_revision != child.definition_revision + + +def test_batch_failure_and_postwrite_failure_preserve_revision( + backend: ConstraintProvider, monkeypatch: pytest.MonkeyPatch +) -> None: + with client() as c: + token = attach(c) + create(c, token) + original = get(c, token) + with pytest.raises(CatalogClientError): + apply( + c, + token, + original, + add(definition(ConstraintKind.UNIQUE, "id")), + add(definition(ConstraintKind.UNIQUE, "missing")), + ) + assert get(c, token) == original + + def fail(self: ConstraintCatalog) -> None: + raise ValueError("injected postwrite failure") + + monkeypatch.setattr(type(backend.authority), "_after_write", fail) + with pytest.raises(CatalogClientError, match="injected"): + apply(c, token, original, add(definition(ConstraintKind.UNIQUE, "id"))) + assert get(c, token) == original diff --git a/tests/catalog/test_constraints_reference.py b/tests/catalog/test_constraints_reference.py new file mode 100644 index 00000000..82c99858 --- /dev/null +++ b/tests/catalog/test_constraints_reference.py @@ -0,0 +1,470 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Semantic proofs for canonical constraints, without advertising public RPCs.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path +from typing import Any, cast + +import duckdb +import pyarrow as pa +import pytest + +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, + ConstraintValidation, + ForeignKeyDefinition, + ForeignKeyMatch, + ReferentialAction, +) +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import SqlExpression + +from .constraints_reference import ConstraintCatalog +from .typing_helpers import present + +ATTACH = b"constraint-owner" +COLUMNS = pa.schema( + cast( + "list[pa.Field[Any]]", + [pa.field("id", pa.int64()), pa.field("other", pa.int64()), pa.field("label", pa.string())], + ) +) + + +@pytest.fixture +def catalog(tmp_path: Path) -> ConstraintCatalog: + return ConstraintCatalog(str(tmp_path / "constraints.sqlite")) + + +def table(name: str = "items", path: list[str] | None = None) -> CatalogObjectRef: + return CatalogObjectRef(kind="table", schema_path=path or ["main"], name=name) + + +def definition(kind: ConstraintKind, *columns: str, name: str | None = None) -> ConstraintDefinition: + return ConstraintDefinition(kind=kind, column_paths=[[column] for column in columns], declared_name=name) + + +def check(sql: str, *, owner: CatalogObjectRef | None = None) -> ConstraintDefinition: + return ConstraintDefinition( + kind=ConstraintKind.CHECK, + check_expression=SqlExpression( + sql=sql, + profile=ROW_SCALAR_EXPRESSION, + schema_path=(owner or table()).schema_path, + evaluation_context=QueryEvaluationContext(), + ), + ) + + +def foreign_key(parent: CatalogObjectRef, local: list[str], remote: list[str]) -> ConstraintDefinition: + return ConstraintDefinition( + kind=ConstraintKind.FOREIGN_KEY, + column_paths=[[name] for name in local], + foreign_key=ForeignKeyDefinition(referenced_table=parent, referenced_column_paths=[[name] for name in remote]), + ) + + +def add(item: ConstraintDefinition, **kwargs: Any) -> ConstraintChange: + return ConstraintChange( + operation=ConstraintOperation.ADD, + definition=item, + enforcement=kwargs.get("enforcement", ConstraintEnforcement.ENFORCED), + validation=kwargs.get("validation", ConstraintValidation.ALL_ROWS), + ) + + +def mutate( + catalog: ConstraintCatalog, + target: CatalogObjectRef, + *changes: ConstraintChange, + tx: bytes | None = None, + revision: bytes | None = None, +) -> ConstraintSnapshot: + return catalog.apply( + ConstraintsApplyRequest( + attach_opaque_data=ATTACH, + transaction_opaque_data=tx, + table=target, + expected_revision=revision or catalog.get(ATTACH, target, tx).definition_revision, + changes=[change.serialize_to_bytes() for change in changes], + ) + ) + + +def write( + catalog: ConstraintCatalog, target: CatalogObjectRef, rows: list[dict[str, Any]], *, tx: bytes | None = None +) -> None: + catalog.replace_rows(ATTACH, target, pa.Table.from_pylist(rows, schema=catalog.columns(ATTACH, target, tx)), tx=tx) + + +def test_canonical_create_readback_survives_restart_and_ipc(catalog: ConstraintCatalog) -> None: + target = table("literal.table", ["literal.schema"]) + columns = COLUMNS.with_metadata({b"provider": b"kept"}) + requested = [definition(ConstraintKind.UNIQUE, "id", name="user_label"), check("id > 0", owner=target)] + created = catalog.create(ATTACH, target, columns, requested) + assert created == ConstraintSnapshot.deserialize_from_bytes(created.serialize_to_bytes()) + assert [info.definition for info in created.constraint_infos()] == requested + assert all(info.trusted and info.ref.owner_table == "literal.table" for info in created.constraint_infos()) + assert all(info.ref.name != "user_label" for info in created.constraint_infos()) + restarted = ConstraintCatalog(catalog.database_path) + assert restarted.get(ATTACH, target) == created + assert restarted.columns(ATTACH, target).equals(columns, check_metadata=True) + write(restarted, target, [{"id": 1, "label": "persisted"}]) + assert ConstraintCatalog(catalog.database_path).rows(ATTACH, target).to_pylist() == [ + {"id": 1, "other": None, "label": "persisted"} + ] + + +def test_unique_composite_null_semantics_and_atomic_batch_failure(catalog: ConstraintCatalog) -> None: + target = table() + catalog.create(ATTACH, target, COLUMNS, [definition(ConstraintKind.UNIQUE, "id", "other")]) + accepted: list[dict[str, Any]] = [ + {"id": 1, "other": None}, + {"id": 1, "other": None}, + {"id": None, "other": 2}, + {"id": 1, "other": 2}, + ] + write(catalog, target, accepted) + before, version = catalog.rows(ATTACH, target), catalog.version(ATTACH) + with pytest.raises(ValueError, match="UNIQUE"): + write(catalog, target, [{"id": 9, "other": 9}, {"id": 9, "other": 9}]) + assert catalog.rows(ATTACH, target).equals(before) + assert catalog.version(ATTACH) == version + + +def test_primary_and_independent_not_null_drive_discovered_schema(catalog: ConstraintCatalog) -> None: + target = table() + columns = COLUMNS.set(0, COLUMNS.field("id").with_nullable(False)) + created = catalog.create( + ATTACH, + target, + columns, + [definition(ConstraintKind.PRIMARY_KEY, "id"), definition(ConstraintKind.NOT_NULL, "id")], + ) + primary, not_null = created.constraint_infos() + with pytest.raises(ValueError, match="NOT_NULL"): + write(catalog, target, [{"id": None}]) + with pytest.raises(ValueError, match="UNIQUE"): + write(catalog, target, [{"id": 1}, {"id": 1}]) + write(catalog, target, [{"id": 1}]) + mutate(catalog, target, ConstraintChange(operation=ConstraintOperation.DROP, ref=primary.ref, cascade=False)) + assert not catalog.columns(ATTACH, target).field("id").nullable + mutate(catalog, target, ConstraintChange(operation=ConstraintOperation.DROP, ref=not_null.ref, cascade=False)) + assert catalog.columns(ATTACH, target).field("id").nullable + write(catalog, target, [{"id": None}]) + + +@pytest.mark.parametrize("has_constraint", [False, True]) +def test_creation_rejects_inconsistent_arrow_nullability(catalog: ConstraintCatalog, has_constraint: bool) -> None: + columns = COLUMNS if has_constraint else COLUMNS.set(0, COLUMNS.field("id").with_nullable(False)) + constraints = [definition(ConstraintKind.NOT_NULL, "id")] if has_constraint else [] + with pytest.raises(ValueError, match="Arrow nullability"): + catalog.create(ATTACH, table(), columns, constraints) + with pytest.raises(ValueError, match="does not exist"): + catalog.get(ATTACH, table()) + + +def test_check_binds_and_evaluates_typed_null_true_and_false(catalog: ConstraintCatalog) -> None: + target = table() + catalog.create(ATTACH, target, COLUMNS, [check("id > 0 AND (label IS NULL OR lower(label) = 'ok')")]) + write(catalog, target, [{"id": None, "label": None}, {"id": 2, "label": "OK"}]) + before = catalog.rows(ATTACH, target) + assert before.schema.field("id").type == pa.int64() + with pytest.raises(ValueError, match="CHECK"): + write(catalog, target, [{"id": -1, "label": "ok"}]) + assert catalog.rows(ATTACH, target).equals(before) + mutate(catalog, target, add(check("NULL::BOOLEAN"))) + write(catalog, target, [{"id": None, "label": "ok"}]) + + +@pytest.mark.parametrize( + "sql", ["random() > 0", "(SELECT true)", "missing > 0", "id", "id > 0); DELETE FROM items; SELECT (true"] +) +def test_unqualified_checks_reject_even_on_empty_tables(catalog: ConstraintCatalog, sql: str) -> None: + version = catalog.version(ATTACH) + with pytest.raises((ValueError, duckdb.Error)): + catalog.create(ATTACH, table(), COLUMNS, [check(sql)]) + assert catalog.version(ATTACH) == version + + +@pytest.mark.parametrize( + "kind", [ConstraintKind.NOT_NULL, ConstraintKind.UNIQUE, ConstraintKind.PRIMARY_KEY, ConstraintKind.CHECK] +) +def test_add_validates_existing_rows_before_publishing(catalog: ConstraintCatalog, kind: ConstraintKind) -> None: + target = table() + catalog.create(ATTACH, target, COLUMNS, []) + write(catalog, target, [{"id": None}, {"id": -1}, {"id": -1}]) + before, rows, version = catalog.get(ATTACH, target), catalog.rows(ATTACH, target), catalog.version(ATTACH) + candidate = check("id > 0") if kind == ConstraintKind.CHECK else definition(kind, "id") + with pytest.raises(ValueError): + mutate(catalog, target, add(candidate)) + assert catalog.get(ATTACH, target) == before + assert catalog.rows(ATTACH, target).equals(rows) + assert catalog.version(ATTACH) == version + assert catalog.columns(ATTACH, target).field("id").nullable + + +def test_atomic_apply_stale_revisions_and_stable_label_identity(catalog: ConstraintCatalog) -> None: + target = table() + first = catalog.create(ATTACH, target, COLUMNS, [definition(ConstraintKind.UNIQUE, "id", name="old")]) + write(catalog, target, [{"id": 1, "other": None}]) + info = first.constraint_infos()[0] + renamed = mutate( + catalog, target, ConstraintChange(operation=ConstraintOperation.RENAME, ref=info.ref, declared_name="new") + ) + assert renamed.constraint_infos()[0].ref == info.ref + assert renamed.constraint_infos()[0].definition.declared_name == "new" + assert renamed.definition_revision != first.definition_revision + with pytest.raises(ValueError, match="stale"): + mutate(catalog, target, add(check("id > 0")), revision=first.definition_revision) + with pytest.raises(ValueError, match="NOT_NULL"): + mutate( + catalog, + target, + ConstraintChange(operation=ConstraintOperation.RENAME, ref=info.ref, declared_name="not_published"), + add(definition(ConstraintKind.NOT_NULL, "other")), + ) + assert catalog.get(ATTACH, target) == renamed + version = catalog.version(ATTACH) + unchanged = mutate( + catalog, target, ConstraintChange(operation=ConstraintOperation.RENAME, ref=info.ref, declared_name="new") + ) + assert unchanged == renamed and catalog.version(ATTACH) == version + + +def seed_fk(catalog: ConstraintCatalog) -> tuple[CatalogObjectRef, CatalogObjectRef, ConstraintInfo, ConstraintInfo]: + parent, child = table("parent"), table("child") + parent_info = catalog.create(ATTACH, parent, COLUMNS, [definition(ConstraintKind.UNIQUE, "id", "other")]) + write(catalog, parent, [{"id": 1, "other": 2}]) + child_info = catalog.create(ATTACH, child, COLUMNS, [foreign_key(parent, ["id", "other"], ["id", "other"])]) + return parent, child, parent_info.constraint_infos()[0], child_info.constraint_infos()[0] + + +def test_foreign_key_simple_null_rows_and_referenced_mutations(catalog: ConstraintCatalog) -> None: + parent, child, _, _ = seed_fk(catalog) + write(catalog, child, [{"id": 1, "other": 2}, {"id": 999, "other": None}, {"id": None, "other": 999}]) + with pytest.raises(ValueError, match="FOREIGN_KEY"): + write(catalog, child, [{"id": 999, "other": 2}]) + before = catalog.rows(ATTACH, parent) + with pytest.raises(ValueError, match="FOREIGN_KEY"): + write(catalog, parent, []) + with pytest.raises(ValueError, match="FOREIGN_KEY"): + write(catalog, parent, [{"id": 1, "other": 3}]) + assert catalog.rows(ATTACH, parent).equals(before) + + +def test_adding_fk_validates_existing_data_atomically(catalog: ConstraintCatalog) -> None: + parent, child = table("parent"), table("child") + catalog.create(ATTACH, parent, COLUMNS, [definition(ConstraintKind.UNIQUE, "id")]) + catalog.create(ATTACH, child, COLUMNS, []) + write(catalog, child, [{"id": 8}]) + before = catalog.get(ATTACH, child) + with pytest.raises(ValueError, match="FOREIGN_KEY"): + mutate(catalog, child, add(foreign_key(parent, ["id"], ["id"]))) + assert catalog.get(ATTACH, child) == before + + +def test_explicit_fk_target_change_rejects_without_retaining_a_misleading_binding(catalog: ConstraintCatalog) -> None: + parent, child, key, fk = seed_fk(catalog) + alternate = table("alternate") + catalog.create(ATTACH, alternate, COLUMNS, [definition(ConstraintKind.UNIQUE, "id", "other")]) + before = catalog.get(ATTACH, child) + with pytest.raises(ValueError, match="changing an existing foreign-key target"): + mutate( + catalog, + child, + ConstraintChange( + operation=ConstraintOperation.REPLACE, + ref=fk.ref, + definition=foreign_key(alternate, ["id", "other"], ["id", "other"]), + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + ), + ) + assert catalog.get(ATTACH, child) == before + assert before.constraint_infos()[0].referenced_constraint == key.ref + assert present(before.constraint_infos()[0].definition.foreign_key).referenced_table == parent + + +def test_drop_self_referencing_table_removes_its_owned_fk_under_restrict(catalog: ConstraintCatalog) -> None: + target = table() + created = catalog.create( + ATTACH, + target, + COLUMNS, + [foreign_key(target, ["other"], ["id"]), definition(ConstraintKind.UNIQUE, "id")], + ) + assert created.constraint_infos()[0].referenced_constraint == created.constraint_infos()[1].ref + write(catalog, target, [{"id": 1, "other": 1}]) + catalog.drop_table(ATTACH, target) + with pytest.raises(ValueError, match="does not exist"): + catalog.get(ATTACH, target) + + +def test_fk_dependency_never_retargets_and_cascade_updates_both_revisions(catalog: ConstraintCatalog) -> None: + parent, child, key, fk = seed_fk(catalog) + mutate(catalog, parent, add(definition(ConstraintKind.UNIQUE, "id", "other", name="alternate"))) + before_parent, before_child = catalog.get(ATTACH, parent), catalog.get(ATTACH, child) + with pytest.raises(ValueError, match="dependent foreign keys"): + mutate(catalog, parent, ConstraintChange(operation=ConstraintOperation.DROP, ref=key.ref, cascade=False)) + with pytest.raises(ValueError, match="replacement invalidates"): + mutate( + catalog, + parent, + ConstraintChange( + operation=ConstraintOperation.REPLACE, + ref=key.ref, + definition=definition(ConstraintKind.UNIQUE, "label"), + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + ), + ) + assert catalog.get(ATTACH, parent) == before_parent + assert catalog.get(ATTACH, child).constraint_infos()[0].referenced_constraint == key.ref + mutate(catalog, parent, ConstraintChange(operation=ConstraintOperation.DROP, ref=key.ref, cascade=True)) + assert catalog.get(ATTACH, child).constraints == [] + assert catalog.get(ATTACH, child).definition_revision != before_child.definition_revision + assert all(item.ref != key.ref for item in catalog.get(ATTACH, parent).constraint_infos()) + assert fk.referenced_constraint == key.ref + + +def test_table_rename_rewrites_fk_coordinates_and_recreate_uses_new_keys(catalog: ConstraintCatalog) -> None: + parent, child, key, _ = seed_fk(catalog) + renamed = catalog.rename_table(ATTACH, parent, "renamed") + new_parent = table("renamed") + assert renamed.constraint_infos()[0].ref.name == key.ref.name + assert renamed.constraint_infos()[0].ref.owner_table == "renamed" + child_fk = catalog.get(ATTACH, child).constraint_infos()[0] + assert child_fk.referenced_constraint == renamed.constraint_infos()[0].ref + assert present(child_fk.definition.foreign_key).referenced_table == new_parent + write(catalog, child, [{"id": 1, "other": 2}]) + with pytest.raises(ValueError, match="dependent foreign keys"): + catalog.drop_table(ATTACH, new_parent) + catalog.drop_table(ATTACH, new_parent, cascade=True) + fresh = catalog.create(ATTACH, new_parent, COLUMNS, [definition(ConstraintKind.UNIQUE, "id", "other")]) + assert fresh.constraint_infos()[0].ref.name != key.ref.name + assert fresh.definition_revision != renamed.definition_revision + + +def test_rollback_commit_reconnect_and_transaction_conflicts(catalog: ConstraintCatalog) -> None: + target = table() + original = catalog.create(ATTACH, target, COLUMNS, []) + tx = catalog.begin(ATTACH) + staged = mutate(catalog, target, add(definition(ConstraintKind.UNIQUE, "id")), tx=tx) + write(catalog, target, [{"id": 1}], tx=tx) + assert catalog.get(ATTACH, target) == original + restarted = ConstraintCatalog(catalog.database_path) + assert restarted.get(ATTACH, target, tx) == staged + with pytest.raises(ValueError, match="owner"): + restarted.get(b"another-owner", target, tx) + restarted.rollback(ATTACH, tx) + restarted.rollback(ATTACH, tx) + assert restarted.get(ATTACH, target) == original + assert restarted.rows(ATTACH, target).num_rows == 0 + with pytest.raises(ValueError, match="terminal"): + restarted.commit(ATTACH, tx) + winner, loser = restarted.begin(ATTACH), restarted.begin(ATTACH) + mutate(restarted, target, add(definition(ConstraintKind.UNIQUE, "id")), tx=winner) + mutate(restarted, target, add(check("id > 0")), tx=loser) + write(restarted, target, [{"id": 7}], tx=winner) + restarted.commit(ATTACH, winner) + restarted.commit(ATTACH, winner) + with pytest.raises(ValueError, match="conflict"): + restarted.commit(ATTACH, loser) + restarted.rollback(ATTACH, loser) + persisted = ConstraintCatalog(catalog.database_path) + assert persisted.get(ATTACH, target).constraint_infos()[0].definition.kind == ConstraintKind.UNIQUE + assert persisted.rows(ATTACH, target).column("id").to_pylist() == [7] + + +def test_failed_statement_and_post_write_fault_preserve_prior_state( + catalog: ConstraintCatalog, monkeypatch: pytest.MonkeyPatch +) -> None: + target = table() + catalog.create(ATTACH, target, COLUMNS, [definition(ConstraintKind.UNIQUE, "id")]) + tx = catalog.begin(ATTACH) + write(catalog, target, [{"id": 1}], tx=tx) + with pytest.raises(ValueError, match="UNIQUE"): + write(catalog, target, [{"id": 2}, {"id": 2}], tx=tx) + catalog.commit(ATTACH, tx) + before, version = catalog.get(ATTACH, target), catalog.version(ATTACH) + + def fail() -> None: + raise RuntimeError("injected after SQLite write") + + monkeypatch.setattr(catalog, "_after_write", fail) + with pytest.raises(RuntimeError, match="injected"): + mutate(catalog, target, add(check("id > 0"))) + restarted = ConstraintCatalog(catalog.database_path) + assert restarted.get(ATTACH, target) == before + assert restarted.version(ATTACH) == version + assert restarted.rows(ATTACH, target).column("id").to_pylist() == [1] + + +@pytest.mark.parametrize( + "metadata_key", + [b"vgi.column.default.v1", b"vgi.column.generated.v1", b"vgi.column.generation.v1", b"default", b"generated"], +) +def test_unimplemented_column_semantics_reject_before_creation(catalog: ConstraintCatalog, metadata_key: bytes) -> None: + columns = COLUMNS.set(0, COLUMNS.field("id").with_metadata({metadata_key: b"unimplemented"})) + version = catalog.version(ATTACH) + with pytest.raises(ValueError, match="column semantics"): + catalog.create(ATTACH, table(), columns, []) + assert catalog.version(ATTACH) == version + + +@pytest.mark.parametrize("mode", ["metadata", "future", "nested", "match_full", "cascade", "float", "unbound_fk"]) +def test_unsupported_reference_scope_rejects_without_publication(catalog: ConstraintCatalog, mode: str) -> None: + target = table() + catalog.create(ATTACH, target, COLUMNS, []) + before, version = catalog.get(ATTACH, target), catalog.version(ATTACH) + with pytest.raises(ValueError): + if mode == "metadata": + mutate( + catalog, + target, + add(definition(ConstraintKind.UNIQUE, "id"), enforcement=ConstraintEnforcement.METADATA_ONLY), + ) + elif mode == "future": + mutate( + catalog, + target, + add(definition(ConstraintKind.UNIQUE, "id"), validation=ConstraintValidation.FUTURE_WRITES), + ) + elif mode == "nested": + mutate( + catalog, target, add(ConstraintDefinition(kind=ConstraintKind.UNIQUE, column_paths=[["id", "child"]])) + ) + elif mode == "float": + catalog.create(ATTACH, table("floating"), pa.schema([pa.field("x", pa.float64())]), []) + elif mode == "unbound_fk": + mutate(catalog, target, add(foreign_key(target, ["id"], ["id"]))) + else: + # The target key is in the same atomic candidate, but an unsupported + # action/match must still roll back that otherwise-valid addition. + fk = foreign_key(target, ["other"], ["id"]) + assert fk.foreign_key is not None + fk = replace( + fk, + foreign_key=replace( + fk.foreign_key, + match=ForeignKeyMatch.FULL if mode == "match_full" else ForeignKeyMatch.SIMPLE, + on_delete=ReferentialAction.CASCADE if mode == "cascade" else ReferentialAction.NO_ACTION, + ), + ) + mutate(catalog, target, add(definition(ConstraintKind.UNIQUE, "id")), add(fk)) + assert catalog.get(ATTACH, target) == before + assert catalog.version(ATTACH) == version diff --git a/tests/catalog/test_declarative.py b/tests/catalog/test_declarative.py index 6c668f9d..7c4b0911 100644 --- a/tests/catalog/test_declarative.py +++ b/tests/catalog/test_declarative.py @@ -14,7 +14,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Annotated, ClassVar +from typing import Annotated, Any, ClassVar import pyarrow as pa import pytest @@ -727,74 +727,58 @@ def test_serialize_foreign_keys_uses_explicit_schema(self) -> None: class TestTableToTableInfoConstraints: - """Tests for Table.to_table_info() with PK and FK constraints.""" - - def test_to_table_info_with_primary_key(self) -> None: - """to_table_info converts PK column names to indices.""" - table = Table( - name="users", - columns=pa.schema( - [("id", pa.int64()), ("email", pa.string())] # type: ignore[arg-type] - ), - primary_key=(("id",),), - ) - info = table.to_table_info(["main"]) - assert info.primary_key_constraints == [[0]] - - def test_to_table_info_with_composite_pk(self) -> None: - """to_table_info converts composite PK column names to indices.""" - table = Table( - name="projects", - columns=pa.schema( - [("dept_id", pa.int64()), ("code", pa.string()), ("title", pa.string())] # type: ignore[arg-type] - ), - primary_key=(("dept_id", "code"),), - ) - info = table.to_table_info(["main"]) - assert info.primary_key_constraints == [[0, 1]] - - def test_to_table_info_with_foreign_key(self) -> None: - """to_table_info serializes FK constraints as IPC bytes.""" - table = Table( - name="employees", - columns=pa.schema([("id", pa.int64()), ("dept_id", pa.int64())]), - foreign_key=( - ForeignKeyDef(columns=("dept_id",), referenced_table="departments", referenced_columns=("id",)), + """Canonical records preserve provider identity and trust without inference.""" + + @pytest.mark.parametrize( + "legacy", + [ + {"not_null": ("id",)}, + {"unique": (("id",),)}, + {"primary_key": (("id",),)}, + {"check": ("id > 0",)}, + {"foreign_key": (ForeignKeyDef(columns=("id",), referenced_table="other", referenced_columns=("id",)),)}, + ], + ) + def test_legacy_discovery_requires_explicit_migration(self, legacy: dict[str, Any]) -> None: + """Old declarations cannot invent provider identity or enforcement.""" + table = Table(name="users", columns=pa.schema([("id", pa.int64())]), **legacy) + with pytest.raises(ValueError, match="explicit canonical"): + table.to_table_info(["main"]) + + def test_canonical_records_preserve_identity_and_state(self) -> None: + """Readback carries the original provider facts unchanged.""" + from vgi.catalog.constraints import ( + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintValidation, + ) + from vgi.catalog.dependencies import CatalogObjectRef + + record = ConstraintInfo( + ref=CatalogObjectRef(kind="constraint", schema_path=["main"], owner_table="users", name="provider-key"), + definition=ConstraintDefinition( + kind=ConstraintKind.UNIQUE, column_paths=[["id"]], declared_name="declared" ), + enforcement=ConstraintEnforcement.METADATA_ONLY, + validation=ConstraintValidation.UNVALIDATED, ) - info = table.to_table_info(["data"]) - assert len(info.foreign_key_constraints) == 1 - assert isinstance(info.foreign_key_constraints[0], bytes) - - def test_to_table_info_no_pk_fk_defaults_empty(self) -> None: - """to_table_info returns empty PK/FK when not specified.""" table = Table( - name="simple", + name="users", columns=pa.schema([("id", pa.int64())]), + constraints=(record,), + definition_revision=b"revision", ) info = table.to_table_info(["main"]) - assert info.primary_key_constraints == [] - assert info.foreign_key_constraints == [] + assert info.constraints == [record.serialize_to_bytes()] + assert info.constraint_infos() == [record] + assert info.definition_revision == b"revision" - def test_to_table_info_all_constraints(self) -> None: - """to_table_info converts all constraint types.""" - table = Table( - name="full", - columns=pa.schema( - [("id", pa.int64()), ("name", pa.string()), ("ref_id", pa.int64())] # type: ignore[arg-type] - ), - not_null=("id", "name"), - unique=(("name",),), - check=("id > 0",), - primary_key=(("id",),), - foreign_key=(ForeignKeyDef(columns=("ref_id",), referenced_table="other", referenced_columns=("id",)),), - ) - info = table.to_table_info(["main"]) - assert info.not_null_constraints == [0, 1] - assert info.unique_constraints == [[1]] - assert info.check_constraints == ["id > 0"] - assert info.primary_key_constraints == [[0]] - assert len(info.foreign_key_constraints) == 1 + def test_no_constraints_is_explicit(self) -> None: + """Empty declarations serialize as an authoritative empty list.""" + table = Table(name="simple", columns=pa.schema([("id", pa.int64())])) + assert table.to_table_info(["main"]).constraints == [] class TestTableToTableInfo: @@ -817,23 +801,6 @@ def test_to_table_info_basic(self) -> None: assert info.comment == "User table" assert info.tags == {"type": "core"} - def test_to_table_info_with_constraints(self) -> None: - """Table converts constraints to indices.""" - table = Table( - name="users", - columns=pa.schema( - [("id", pa.int64()), ("email", pa.string())] # type: ignore[arg-type] - ), - not_null=("id", "email"), - unique=(("id",), ("email",)), - check=("id > 0",), - ) - info = table.to_table_info(["main"]) - # Column indices: id=0, email=1 - assert info.not_null_constraints == [0, 1] - assert info.unique_constraints == [[0], [1]] - assert info.check_constraints == ["id > 0"] - # ============================================================================= # Table Defaults Tests @@ -871,7 +838,7 @@ def test_defaults_empty_dict(self) -> None: defaults={}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("id").metadata is None def test_defaults_python_str_quoted(self) -> None: @@ -884,7 +851,7 @@ def test_defaults_python_str_quoted(self) -> None: defaults={"name": "unknown"}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("name").metadata == {b"default": b"'unknown'"} def test_defaults_python_str_escapes_quotes(self) -> None: @@ -897,7 +864,7 @@ def test_defaults_python_str_escapes_quotes(self) -> None: defaults={"name": "it's"}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("name").metadata == {b"default": b"'it''s'"} def test_defaults_python_int(self) -> None: @@ -908,7 +875,7 @@ def test_defaults_python_int(self) -> None: defaults={"qty": 42}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("qty").metadata == {b"default": b"42"} def test_defaults_python_float(self) -> None: @@ -919,7 +886,7 @@ def test_defaults_python_float(self) -> None: defaults={"price": 9.99}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("price").metadata == {b"default": b"9.99"} def test_defaults_python_bool(self) -> None: @@ -930,7 +897,7 @@ def test_defaults_python_bool(self) -> None: defaults={"a": True, "b": False}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("a").metadata == {b"default": b"true"} assert schema.field("b").metadata == {b"default": b"false"} @@ -942,7 +909,7 @@ def test_defaults_python_none(self) -> None: defaults={"val": None}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("val").metadata == {b"default": b"NULL"} def test_defaults_sql_expression(self) -> None: @@ -953,7 +920,7 @@ def test_defaults_sql_expression(self) -> None: defaults={"ts": Sql("current_timestamp")}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("ts").metadata == {b"default": b"current_timestamp"} def test_defaults_mixed_types(self) -> None: @@ -970,7 +937,7 @@ def test_defaults_mixed_types(self) -> None: defaults={"qty": 0, "name": "unknown"}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) assert schema.field("qty").metadata == {b"default": b"0"} assert schema.field("name").metadata == {b"default": b"'unknown'"} assert schema.field("id").metadata is None @@ -988,7 +955,7 @@ def test_defaults_preserve_existing_metadata(self) -> None: defaults={"row_id": 0}, ) info = table.to_table_info(["main"]) - schema = pa.ipc.read_schema(pa.BufferReader(info.columns)) # type: ignore[arg-type] + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) metadata = schema.field("row_id").metadata assert metadata is not None assert metadata[b"is_row_id"] == b"" @@ -1125,9 +1092,9 @@ def test_macro_to_macro_info(self) -> None: assert info.name == "add" assert info.schema_path == ["main"] assert info.macro_type == MacroType.SCALAR - assert info.parameters == ["x", "y"] - assert info.parameter_default_values is not None - assert info.definition == "x + y" + assert [p.name for p in info.overloads[0].parameters] == ["x", "y"] + assert info.overloads[0].parameters[1].default_value is not None + assert info.overloads[0].definition == "x + y" assert info.comment == "Add two values" assert info.tags == {"type": "math"} @@ -1558,11 +1525,29 @@ class TestReadOnlyCatalogWithCatalog: @pytest.fixture def users_table(self) -> Table: - """Create a users table for testing.""" + """Create a users table with an explicit untrusted provider declaration.""" + from vgi.catalog.constraints import ( + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintValidation, + ) + from vgi.catalog.dependencies import CatalogObjectRef + return Table( name="users", function=UsersFunction, - not_null=("id",), + constraints=( + ConstraintInfo( + ref=CatalogObjectRef( + kind="constraint", schema_path=["main"], owner_table="users", name="fixture-user-id" + ), + definition=ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[["id"]]), + enforcement=ConstraintEnforcement.METADATA_ONLY, + validation=ConstraintValidation.UNVALIDATED, + ), + ), comment="User accounts", ) diff --git a/tests/catalog/test_dependencies.py b/tests/catalog/test_dependencies.py new file mode 100644 index 00000000..d1eb4690 --- /dev/null +++ b/tests/catalog/test_dependencies.py @@ -0,0 +1,407 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Object identity, dependency validation and order-independent drop closure.""" + +from __future__ import annotations + +import itertools +from dataclasses import replace +from typing import Any, Literal, cast + +import pyarrow as pa +import pytest + +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot, ObjectDependency + +from .typing_helpers import present + + +def ref(name: str, kind: str = "table", path: list[str] | None = None, owner: str | None = None) -> CatalogObjectRef: + return CatalogObjectRef(kind=kind, schema_path=["main"] if path is None else path, name=name, owner_table=owner) + + +def edge( + d: CatalogObjectRef, + s: CatalogObjectRef, + kind: str = "requires", + *, + blocks: bool = True, + paths: list[list[str]] | None = None, + column: list[str] | None = None, +) -> ObjectDependency: + return ObjectDependency( + dependent=d.serialize_to_bytes(), + subject=s.serialize_to_bytes(), + kind=kind, + blocks_alter=blocks, + subject_column_paths=paths, + dependent_column_path=column, + ) + + +def graph(objects: list[CatalogObjectRef], edges: list[ObjectDependency]) -> DependencySnapshot: + return DependencySnapshot( + catalog_version=9, + objects=[r.serialize_to_bytes() for r in objects], + dependencies=[e.serialize_to_bytes() for e in edges], + ) + + +def test_identity_preserves_raw_paths_owner_and_spelling() -> None: + refs = [ + ref("T", path=["a.b"]), + ref("T", path=["a", "b"]), + ref("t", path=["a.b"]), + ref("fire", "trigger", owner="one"), + ref("fire", "trigger", owner="two"), + ref('a."b', "schema", path=[]), + ] + assert len({r.key() for r in refs}) == len(refs) + for r in refs: + assert CatalogObjectRef.deserialize_from_bytes(r.serialize_to_bytes()) == r + + +@pytest.mark.parametrize( + "kwargs", + [ + dict(kind="unknown"), + dict(schema_path=[]), + dict(name=""), + dict(name="a\0b"), + dict(schema_path=[""]), + dict(schema_path="a.b"), + dict(owner_table="t"), + dict(kind="trigger"), + dict(kind="trigger", owner_table=""), + ], +) +def test_malformed_object_references_reject(kwargs: dict[str, Any]) -> None: + with pytest.raises((ValueError, TypeError)): + CatalogObjectRef(**(dict(kind="table", schema_path=["main"], name="t") | kwargs)) + + +def test_restrict_checks_blockers_after_the_complete_owned_closure() -> None: + a, b, c, external = [ref(n) for n in "abcd"] + edges = [edge(b, a, "owned"), edge(c, b, "owned"), edge(c, a)] + for order in itertools.permutations(edges): + g = graph([a, b, c, external], list(order)) + assert g.drop_set(a) == [a, b, c] + assert g.drop_set(c) == [c] # Dropping a dependent never drops its owner. + blocked = graph([a, b, c, external], [*edges, edge(external, c)]) + with pytest.raises(ValueError, match="dependent"): + blocked.drop_set(a) + assert blocked.drop_set(a, cascade=True) == [a, b, c, external] + + +def test_reference_cycles_cascade_without_recursing_forever() -> None: + a, b, c = [ref(n) for n in "abc"] + g = graph([a, b, c], [edge(a, b), edge(b, a), edge(c, b)]) + with pytest.raises(ValueError, match="dependent"): + g.drop_set(a) + assert g.drop_set(a, cascade=True) == [a, b, c] + assert g.drop_set(c) == [c] + + +@pytest.mark.parametrize( + "case", + [ + "duplicate_object", + "dangling", + "conflicting_alter", + "conflicting_edge", + "owners", + "owner_cycle", + "self", + "kind", + "version", + ], +) +def test_invalid_graphs_reject(case: str) -> None: + a, b, c = [ref(n) for n in "abc"] + with pytest.raises(ValueError): + if case == "duplicate_object": + graph([a, a], []) + elif case == "dangling": + graph([a], [edge(a, b)]) + elif case == "conflicting_alter": + graph([a, b], [edge(a, b), edge(a, b, blocks=False)]) + elif case == "conflicting_edge": + graph([a, b], [edge(a, b), edge(a, b, "owned")]) + elif case == "owners": + graph([a, b, c], [edge(a, b, "owned"), edge(a, c, "owned")]) + elif case == "owner_cycle": + graph([a, b], [edge(a, b, "owned"), edge(b, a, "owned")]) + elif case == "self": + edge(a, a) + elif case == "kind": + edge(a, b, "unknown") + else: + replace(graph([a], []), catalog_version=-1) + + +@pytest.mark.parametrize("shape", ["missing", "nullable", "null", "two_rows", "extra_batch", "trailing"]) +def test_wire_requires_complete_single_records(shape: str) -> None: + r = ref("t") + schema = r.ARROW_SCHEMA + row = dict(kind=r.kind, schema_path=r.schema_path, name=r.name, owner_table=r.owner_table) + if shape == "missing": + schema = schema.remove(3) + if shape == "nullable": + schema = schema.set(0, pa.field("kind", pa.string(), nullable=True)) + if shape == "null": + row["kind"] = None + batch = pa.RecordBatch.from_pylist([row] * (2 if shape == "two_rows" else 1), schema=schema) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, schema) as writer: + writer.write_batch(batch) + if shape == "extra_batch": + writer.write_batch(batch) + payload = sink.getvalue().to_pybytes() + (b"extra" if shape == "trailing" else b"") + with pytest.raises(ValueError): + CatalogObjectRef.deserialize_from_bytes(payload) + + +def test_mutated_snapshot_is_revalidated_before_planning() -> None: + a = ref("a") + g = graph([a], []) + g.objects.append(a.serialize_to_bytes()) + with pytest.raises(ValueError, match="duplicate"): + g.drop_set(a) + + +def test_edges_merge_only_within_full_column_identity_and_canonicalize_paths() -> None: + table, view = ref("t"), ref("v", "view") + edges = [ + edge(view, table, paths=[["a", "child"], ["b"]]), + edge(view, table, paths=[["a"]]), + edge(table, table, column=["g1"], paths=[["a"]]), + edge(table, table, column=["g2"], paths=[["a.b"]]), + ] + expected = graph([table, view], edges) + assert len(expected.edges()) == 3 + assert next(item for item in expected.edges() if item.dependent_ref() == view).subject_column_paths == [ + ["a"], + ["a", "child"], + ["b"], + ] + for order in itertools.permutations(edges): + assert graph([table, view], [*order, order[0]]).dependencies == expected.dependencies + merged = graph([table, view], [edge(view, table, paths=[["a"]]), edge(view, table)]) + assert merged.edges()[0].subject_column_paths is None + + +@pytest.mark.parametrize( + "kwargs", + [ + {"blocks_alter": 1}, + {"blocks_alter": None}, + {"subject_column_paths": []}, + {"subject_column_paths": [["a"], ["a"]]}, + {"subject_column_paths": ["a.b"]}, + {"subject_column_paths": [[""]]}, + {"subject_column_paths": [[None]]}, + {"subject_column_paths": [["a\0b"]]}, + {"dependent_column_path": []}, + {"dependent_column_path": "a.b"}, + {"dependent_column_path": [None]}, + ], +) +def test_column_edge_rejects_malformed_flags_and_paths(kwargs: dict[str, Any]) -> None: + with pytest.raises((ValueError, TypeError)): + replace(edge(ref("a"), ref("b")), **kwargs) + + +def test_column_edge_requires_appropriate_object_kinds_and_disjoint_self_targets() -> None: + table, view, index = ref("t"), ref("v", "view"), ref("i", "index") + with pytest.raises(ValueError, match="table reference"): + edge(view, table, column=["generated"], paths=[["value"]]) + with pytest.raises(ValueError, match="table or view"): + edge(table, index, paths=[["value"]]) + for paths in [None, [["a"]], [["a", "b"]], [["a", "b", "c"]]]: + with pytest.raises(ValueError, match="self dependencies"): + edge(table, table, column=["a", "b"], paths=paths) + assert edge(table, table, column=["a.b"], paths=[["a", "b"]]) + assert edge(table, table, column=["a", "left"], paths=[["a", "right"]]) + + +def test_column_validation_proves_exact_schema_identity_not_just_path_syntax() -> None: + table = ref("t") + value = graph([table], [edge(table, table, column=["generated"], paths=[["a.b"], ["a", "b"]])]) + schema = pa.schema( + [ + pa.field("generated", pa.int64()), + pa.field("a.b", pa.int64()), + pa.field("a", pa.struct([pa.field("b", pa.int64())])), + ] + ) + value.validate_columns({table.key(): schema}) + assert DependencySnapshot.deserialize_from_bytes(value.serialize_to_bytes()) == value + with pytest.raises(ValueError, match="authoritative schema"): + value.validate_columns({}) + with pytest.raises(ValueError, match="missing or ambiguous"): + value.validate_columns({table.key(): schema.remove(1)}) + with pytest.raises(ValueError, match="non-struct"): + value.validate_columns({table.key(): schema.set(2, pa.field("a", pa.list_(pa.int64())))}) + with pytest.raises(ValueError, match="missing or ambiguous"): + value.validate_columns({table.key(): schema.append(schema.field("a.b"))}) + # A valid parent does not excuse a separately claimed nonexistent child. + invalid_child = graph([table], [edge(table, table, column=["generated"], paths=[["a"], ["a", "missing"]])]) + with pytest.raises(ValueError, match="missing or ambiguous"): + invalid_child.validate_columns({table.key(): schema}) + + +@pytest.mark.parametrize("action", ["rename", "type", "drop"]) +def test_semantic_protection_is_symmetric_for_parent_child_but_not_dotted_names( + action: Literal["rename", "type", "drop"], +) -> None: + table, view = ref("t"), ref("v", "view") + value = graph([table, view], [edge(view, table, paths=[["a", "b"]])]) + for path in [["a"], ["a", "b"], ["a", "b", "child"]]: + assert value.alter_blockers(table, column_path=path, action=action) + with pytest.raises(ValueError, match="dependent"): + value.assert_alter_allowed(table, column_path=path, action=action) + for path in [["a.b"], ["a", "c"], ["A", "b"]]: + assert value.alter_blockers(table, column_path=path, action=action) == [] + value.assert_alter_allowed(table, column_path=path, action=action) + assert value.alter_blockers(table, action=action) + + +def test_nonblocking_alter_edges_still_protect_drop_and_whole_subject_protects_all_columns() -> None: + table, view = ref("t"), ref("v", "view") + value = graph([table, view], [edge(view, table, blocks=False, paths=[["source"]])]) + value.assert_alter_allowed(table, column_path=["source"], action="rename") + value.assert_alter_allowed(table, column_path=["source"], action="type") + with pytest.raises(ValueError, match="dependent"): + value.plan_alter(table, column_path=["source"], action="drop") + plan = value.plan_alter(table, column_path=["source"], action="drop", cascade=True) + assert plan.drop_objects == [view] + assert [(drop.table, drop.column_path) for drop in plan.drop_columns] == [(table, ["source"])] + whole = graph([table, view], [edge(view, table)]) + assert whole.alter_blockers(table, column_path=["anything", "deep"]) + + +def test_generated_columns_drop_in_order_independent_closure_without_dropping_the_table() -> None: + table, view, external = ref("t"), ref("v", "view"), ref("other") + edges = [ + edge(table, table, column=["generated"], paths=[["source"]]), + edge(table, table, column=["next"], paths=[["generated"]]), + edge(view, table, paths=[["next"]]), + edge(external, table, column=["copied"], paths=[["next"]]), + ] + for order in itertools.permutations(edges): + value = graph([table, view, external], list(order)) + with pytest.raises(ValueError, match="dependent"): + value.plan_alter(table, column_path=["source"], action="drop") + result = value.plan_alter(table, column_path=["source"], action="drop", cascade=True) + assert result.drop_objects == [view] + assert {(drop.table.name, tuple(drop.column_path)) for drop in result.drop_columns} == { + ("t", ("source",)), + ("t", ("generated",)), + ("t", ("next",)), + ("other", ("copied",)), + } + leaf = graph([table], [edge(table, table, column=["generated"], paths=[["source"]])]) + assert leaf.plan_alter(table, column_path=["generated"], action="drop").drop_objects == [] + assert leaf.drop_set(table) == [table] + + +def test_owned_column_deletions_close_before_restrict_and_parent_drop_subsumes_children() -> None: + table, auxiliary = ref("t"), ref("aux") + edges = [ + edge(auxiliary, table, "owned", column=["record", "child"], paths=[["source"]]), + edge(auxiliary, table, "owned", column=["record"], paths=[["source"]]), + edge(auxiliary, auxiliary, column=["record", "other"], paths=[["record", "child"]]), + ] + value = graph([table, auxiliary], edges) + plan = value.plan_alter(table, column_path=["source"], action="drop") + assert plan.drop_objects == [] + assert {(drop.table.name, tuple(drop.column_path)) for drop in plan.drop_columns} == { + ("t", ("source",)), + ("aux", ("record",)), + } + + +def test_whole_object_drop_refuses_to_flatten_an_external_column_into_table_deletion() -> None: + table, dependent = ref("t"), ref("dependent") + value = graph([table, dependent], [edge(dependent, table, column=["generated"], paths=[["source"]])]) + with pytest.raises(ValueError, match="dependent"): + value.drop_set(table) + with pytest.raises(ValueError, match="object-only"): + value.drop_set(table, cascade=True) + plan = value.plan_alter(table, action="drop", cascade=True) + assert plan.drop_objects == [table] + assert [(drop.table, drop.column_path) for drop in plan.drop_columns] == [(dependent, ["generated"])] + + +def test_column_cycles_include_parent_overlap_and_mixed_object_column_nodes() -> None: + a, b, view = ref("a"), ref("b"), ref("v", "view") + scenarios = [ + [edge(a, a, column=["x"], paths=[["y"]]), edge(a, a, column=["y"], paths=[["x"]])], + [edge(a, b, column=["x"], paths=[["y"]]), edge(b, a, column=["y", "child"], paths=[["x"]])], + [edge(a, view, column=["x"]), edge(view, a, paths=[["x"]])], + ] + for edges in scenarios: + for order in itertools.permutations(edges): + with pytest.raises(ValueError, match="column dependency cycle"): + graph([a, b, view], list(order)) + # Distinct columns in one table must not collapse into an object-level cycle. + graph([a, b], [edge(a, b, column=["x"], paths=[["y"]]), edge(b, a, column=["y"], paths=[["z"]])]) + + +def test_object_lifetime_cycles_do_not_turn_independent_generated_columns_into_cycles() -> None: + table, index = ref("t"), ref("i", "index") + edges = [ + edge(table, index), + edge(index, table, "owned"), + edge(table, table, column=["generated"], paths=[["source"]]), + ] + for order in itertools.permutations(edges): + value = graph([table, index], list(order)) + assert value.drop_set(table) == sorted([table, index], key=lambda item: item.key()) + # A view output still depends on its whole definition. An owned view can + # bind its owner's generated column and form a real evaluation cycle. + view = ref("v", "view") + with pytest.raises(ValueError, match="column dependency cycle"): + graph( + [table, view], + [ + edge(table, view, column=["generated"], paths=[["output"]]), + edge(view, table, "owned", paths=[["generated"]]), + ], + ) + # Subject paths scope ALTER protection, never weaken object ownership. + other = ref("other") + with pytest.raises(ValueError, match="ownership cycle"): + graph( + [table, other], [edge(table, other, "owned", paths=[["id"]]), edge(other, table, "owned", paths=[["id"]])] + ) + + +def test_semantic_plans_recheck_mutation_target_schemas_and_actions() -> None: + table = ref("t") + value = graph([table], []) + schemas = {table.key(): pa.schema([pa.field("present", pa.int64())])} + assert value.plan_alter(table, column_path=["present"], action="rename", schemas=schemas).drop_objects == [] + with pytest.raises(ValueError, match="missing or ambiguous"): + value.plan_alter(table, column_path=["missing"], action="drop", schemas=schemas) + with pytest.raises(ValueError, match="authoritative schema"): + value.plan_alter(table, column_path=["present"], action="drop", schemas={}) + with pytest.raises(ValueError, match="does not exist"): + value.plan_alter(ref("absent"), action="rename") + with pytest.raises(ValueError, match="unknown semantic"): + value.plan_alter(table, action=cast(Literal["rename", "type", "drop"], "comment")) + with pytest.raises(ValueError, match="boolean"): + value.plan_alter(table, action="drop", cascade=cast(bool, 1)) + dependency = edge(table, table, column=["generated"], paths=[["source"]]) + present(dependency.subject_column_paths).append([]) + with pytest.raises(ValueError, match="at least one"): + dependency.serialize_to_bytes() + + +@pytest.mark.parametrize("missing", ["blocks_alter", "subject_column_paths", "dependent_column_path"]) +def test_dependency_wire_requires_new_fields_even_when_nullable(missing: str) -> None: + original = edge(ref("a"), ref("b")) + with pa.ipc.open_stream(original.serialize_to_bytes()) as reader: + batch = reader.read_next_batch() + with pytest.raises(ValueError, match="complete schema"): + ObjectDependency.deserialize_from_batch(batch.remove_column(batch.schema.get_field_index(missing))) diff --git a/tests/catalog/test_dependencies_protocol.py b/tests/catalog/test_dependencies_protocol.py new file mode 100644 index 00000000..7bbe237b --- /dev/null +++ b/tests/catalog/test_dependencies_protocol.py @@ -0,0 +1,268 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Durable cross-client dependency authority through public HTTP and sealed routing.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path +from typing import cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.catalog import OnConflict, SerializedSchema +from vgi.catalog.capabilities import CATALOG_DEPENDENCIES_READ, SCHEMA_LIFECYCLE, TRANSACTION_DDL +from vgi.catalog.catalog_interface import AttachOpaqueData +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot +from vgi.client import Client +from vgi.client.catalog_mixin import CatalogClientError +from vgi.exceptions import CatalogCapabilityError, OpaqueDataNotRecognizedError +from vgi.meta_worker import MetaWorker +from vgi.protocol import CatalogAttachRequest, ClientCapabilities, VgiProtocol +from vgi.serve import create_app + +from .dependencies_reference import DEPENDENCY_PROFILES, DependencyCatalog, DependencyWorker +from .schema_options_reference import values +from .typing_helpers import present + +KEY = b"dependency-reference-signing-key" +OFFER = ClientCapabilities( + engine="reference", + native_formats=[], + catalogs=[], + can_stream=False, + filter_encodings=[], + catalog=DEPENDENCY_PROFILES, +) +COLUMNS = SerializedSchema(pa.schema([pa.field("a", pa.int64())]).serialize().to_pybytes()) + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> DependencyWorker: + monkeypatch.setattr(DependencyCatalog, "database_path", str(tmp_path / "dependencies.sqlite"), raising=False) + worker = DependencyWorker(quiet=True) + worker._signing_key = KEY + return worker + + +def request(offer: ClientCapabilities | None = OFFER) -> CatalogAttachRequest: + return CatalogAttachRequest( + name="dependencies", data_version_spec=None, implementation_version=None, client_capabilities=offer + ) + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(create_app(DependencyWorker, describe=False, signing_key=KEY)) + ) + + +def attach(c: Client) -> AttachOpaqueData: + return c.catalog_attach( + name="dependencies", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ).attach_opaque_data + + +def seed(c: Client, token: AttachOpaqueData) -> None: + for path in [["a.b"], ["a"], ["a", "b"], ["outside"]]: + c.schema_create( + attach_opaque_data=AttachOpaqueData(token), + path=path, + comment="kept", + tags={"owner": "one"}, + options=values(limit=25), + ) + c.table_create(attach_opaque_data=AttachOpaqueData(token), schema_path=["a.b"], name="t", columns=COLUMNS) + c.table_create(attach_opaque_data=AttachOpaqueData(token), schema_path=["a", "b"], name="t", columns=COLUMNS) + c.view_create( + attach_opaque_data=AttachOpaqueData(token), + schema_path=["outside"], + name="v", + definition='SELECT * FROM "a.b".t', + ) + c.view_create( + attach_opaque_data=AttachOpaqueData(token), schema_path=["outside"], name="v2", definition="SELECT * FROM v" + ) + + +def identities(snapshot: DependencySnapshot) -> set[tuple[str, tuple[str, ...], str, str]]: + return {r.key() for r in snapshot.object_refs()} + + +def test_resolved_graph_paths_use_the_owning_schema_spelling(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + c.schema_create(attach_opaque_data=token, path=["Root"]) + c.schema_create(attach_opaque_data=token, path=["root", "Child"]) + c.table_create(attach_opaque_data=token, schema_path=["ROOT", "CHILD"], name="T.A", columns=COLUMNS) + c.view_create( + attach_opaque_data=token, + schema_path=["root", "child"], + name="V", + definition='SELECT * FROM "t.a"', + ) + snapshot = c.catalog_dependencies(attach_opaque_data=token) + assert ("schema", ("Root",), "Child", "") in identities(snapshot) + assert ("table", ("Root", "Child"), "T.A", "") in identities(snapshot) + assert ("view", ("Root", "Child"), "V", "") in identities(snapshot) + assert present(c.schema_get(attach_opaque_data=token, path=["ROOT", "child"])).path == ["Root", "Child"] + assert present( + c.table_get(attach_opaque_data=token, schema_path=["root", "child"], name="t.a") + ).schema_path == [ + "Root", + "Child", + ] + with pytest.raises(CatalogClientError, match="dependent"): + c.table_drop(attach_opaque_data=token, schema_path=["root", "CHILD"], name="t.a") + c.table_drop(attach_opaque_data=token, schema_path=["root", "CHILD"], name="t.a", cascade=True) + assert c.view_get(attach_opaque_data=token, schema_path=["ROOT", "child"], name="v") is None + c.schema_drop(attach_opaque_data=token, path=["ROOT"], cascade=True) + assert c.schema_get(attach_opaque_data=token, path=["root", "child"]) is None + + +def test_restrict_replace_cascade_and_literal_paths_are_atomic(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + seed(c, token) + original = c.catalog_dependencies(attach_opaque_data=token) + contents = c.contents(attach_opaque_data=token) + for path in [["a.b"], ["a"]]: + with pytest.raises(CatalogClientError, match="dependent"): + c.schema_drop(attach_opaque_data=token, path=path) + with pytest.raises(CatalogClientError, match="dependent"): + c.schema_create(attach_opaque_data=token, path=path, on_conflict=OnConflict.REPLACE, comment="lost") + assert c.catalog_dependencies(attach_opaque_data=token) == original + assert c.contents(attach_opaque_data=token, if_none_match=contents.etag).not_modified + c.schema_drop(attach_opaque_data=token, path=["a.b"], cascade=True) + changed = c.catalog_dependencies(attach_opaque_data=token) + assert changed.catalog_version == original.catalog_version + 1 + assert c.schema_get(attach_opaque_data=token, path=["a.b"]) is None + assert c.table_get(attach_opaque_data=token, schema_path=["a", "b"], name="t") is not None + assert c.view_get(attach_opaque_data=token, schema_path=["outside"], name="v") is None + assert c.view_get(attach_opaque_data=token, schema_path=["outside"], name="v2") is None + assert c.schema_get(attach_opaque_data=token, path=["outside"]) is not None + assert not c.contents(attach_opaque_data=token, if_none_match=contents.etag).not_modified + c.schema_drop(attach_opaque_data=token, path=["a"], cascade=True) + assert c.schema_get(attach_opaque_data=token, path=["a", "b"]) is None + c.schema_create(attach_opaque_data=token, path=["outside"], on_conflict=OnConflict.REPLACE) + replaced = c.schema_get(attach_opaque_data=token, path=["outside"]) + assert ( + present(replaced).comment is None + and present(replaced).tags == {} + and present(present(replaced).options).explicit_names == [] + ) + + +def test_transaction_visibility_rollback_commit_and_reconnect(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + seed(c, token) + before = c.catalog_dependencies(attach_opaque_data=token) + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.schema_drop(attach_opaque_data=token, path=["a.b"], cascade=True, transaction_opaque_data=tx) + pending = c.catalog_dependencies(attach_opaque_data=token, transaction_opaque_data=tx) + assert identities(pending) < identities(before) + assert c.catalog_dependencies(attach_opaque_data=token) == before + with client() as c: + assert c.catalog_dependencies(attach_opaque_data=token, transaction_opaque_data=tx) == pending + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert c.catalog_dependencies(attach_opaque_data=token) == before + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.schema_drop(attach_opaque_data=token, path=["a.b"], cascade=True, transaction_opaque_data=tx) + with client() as c: + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert identities(c.catalog_dependencies(attach_opaque_data=token)) == identities(pending) + + +def test_discovery_does_not_reserve_or_authorize_a_later_drop(backend: DependencyWorker) -> None: + with client() as c, client() as other: + token, second = attach(c), attach(other) + c.schema_create(attach_opaque_data=token, path=["empty"]) + before = c.catalog_dependencies(attach_opaque_data=token) + other.table_create(attach_opaque_data=second, schema_path=["empty"], name="new", columns=COLUMNS) + with pytest.raises(CatalogClientError, match="dependent"): + c.schema_drop(attach_opaque_data=token, path=["empty"]) + assert c.catalog_dependencies(attach_opaque_data=token).catalog_version == before.catalog_version + 1 + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.schema_drop(attach_opaque_data=token, path=["empty"], cascade=True, transaction_opaque_data=tx) + other.table_create(attach_opaque_data=second, schema_path=["empty"], name="concurrent", columns=COLUMNS) + with pytest.raises(CatalogClientError, match="transaction conflict"): + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert c.table_get(attach_opaque_data=token, schema_path=["empty"], name="concurrent") is not None + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + readonly = c.catalog_transaction_begin(attach_opaque_data=token) + snapshot = c.catalog_dependencies(attach_opaque_data=token, transaction_opaque_data=readonly) + other.schema_create(attach_opaque_data=second, path=["after_snapshot"]) + assert c.catalog_dependencies(attach_opaque_data=token, transaction_opaque_data=readonly) == snapshot + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(readonly)) + assert c.schema_get(attach_opaque_data=token, path=["after_snapshot"]) is not None + + +@pytest.mark.parametrize("failure", ["authorization", "after_write"]) +def test_failed_cascade_preserves_all_metadata_and_versions( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch, failure: str +) -> None: + with client() as c: + token = attach(c) + seed(c, token) + before = c.catalog_dependencies(attach_opaque_data=token) + + def deny(self: DependencyCatalog, ref: CatalogObjectRef | None = None) -> None: + if ref is None or ref.name == "v2": + raise ValueError("denied cascade member") + + monkeypatch.setattr( + DependencyCatalog, "_authorize_drop" if failure == "authorization" else "_after_write", deny + ) + with pytest.raises(CatalogClientError, match="denied"): + c.schema_drop(attach_opaque_data=token, path=["a.b"], cascade=True) + assert c.catalog_dependencies(attach_opaque_data=token) == before + assert c.view_get(attach_opaque_data=token, schema_path=["outside"], name="v2") is not None + + +@pytest.mark.parametrize("meta", [False, True]) +def test_capability_lifecycle_guarantees_ownership_and_routing( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch, meta: bool +) -> None: + worker = cast(VgiProtocol, MetaWorker([backend]) if meta else backend) + legacy = worker.catalog_attach(request(None)).attach_opaque_data + with pytest.raises(CatalogCapabilityError, match=CATALOG_DEPENDENCIES_READ): + worker.catalog_dependencies(attach_opaque_data=legacy) + token = worker.catalog_attach(request()).attach_opaque_data + assert worker.catalog_dependencies(attach_opaque_data=token).catalog_version == 1 + no_tx = replace(OFFER, catalog=replace(DEPENDENCY_PROFILES, transaction=[])) + limited = worker.catalog_attach(request(no_tx)).attach_opaque_data + tx = worker.catalog_transaction_begin(attach_opaque_data=limited).transaction_opaque_data + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + worker.catalog_schema_create(attach_opaque_data=limited, path=["new"], transaction_opaque_data=tx) + with pytest.raises(OpaqueDataNotRecognizedError): + worker.catalog_dependencies(attach_opaque_data=token, transaction_opaque_data=tx) + monkeypatch.setattr(DependencyCatalog, "profiles", replace(DEPENDENCY_PROFILES, ddl=[], read=[])) + with pytest.raises(CatalogCapabilityError, match=SCHEMA_LIFECYCLE): + worker.catalog_schema_drop(attach_opaque_data=token, path=["absent"], ignore_not_found=True) + with pytest.raises(CatalogCapabilityError, match=CATALOG_DEPENDENCIES_READ): + worker.catalog_dependencies(attach_opaque_data=token) + + +def test_missing_and_invalid_requests_preserve_graph(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + before = c.catalog_dependencies(attach_opaque_data=token) + c.schema_drop(attach_opaque_data=token, path=["missing", "child"], ignore_not_found=True, cascade=True) + with pytest.raises(CatalogClientError, match="does not exist"): + c.schema_drop(attach_opaque_data=token, path=["missing"]) + for path in [[], [""], ["bad\0name"]]: + with pytest.raises(CatalogClientError): + c.schema_drop(attach_opaque_data=token, path=path, ignore_not_found=True) + for sql in [ + "SELECT 1", + "SELECT * FROM missing", + "SELECT * FROM main.t WHERE 1=1", + "SELECT * FROM t; SELECT * FROM t", + ]: + with pytest.raises(CatalogClientError): + c.view_create(attach_opaque_data=token, schema_path=["main"], name="bad", definition=sql) + assert c.catalog_dependencies(attach_opaque_data=token) == before diff --git a/tests/catalog/test_dependency_alter_lifecycle.py b/tests/catalog/test_dependency_alter_lifecycle.py new file mode 100644 index 00000000..893e2064 --- /dev/null +++ b/tests/catalog/test_dependency_alter_lifecycle.py @@ -0,0 +1,366 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Atomic semantic ALTER against authoritative object and generated-column edges.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, Literal, cast, overload + +import pyarrow as pa +import pytest + +from vgi.catalog import TableInfo +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.catalog_interface import AttachOpaqueData, SerializedSchema, TransactionOpaqueData +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import ColumnExpressions, SqlExpression, with_column_expressions +from vgi.client import Client +from vgi.client.catalog_mixin import CatalogClientError + +from .dependencies_reference import DependencyCatalog, DependencyWorker +from .test_dependencies_protocol import attach, client +from .test_dependencies_protocol import backend as backend +from .typing_helpers import CatalogColumnTransactionArgs, CatalogObjectTransactionArgs, present + + +def expression(sql: str, *, context: QueryEvaluationContext | None = None) -> SqlExpression: + return SqlExpression( + sql=sql, + profile=ROW_SCALAR_EXPRESSION, + schema_path=["main"], + evaluation_context=context or QueryEvaluationContext(), + ) + + +def generated( + name: str, + sql: str, + *, + generation: Literal["virtual", "stored"] = "virtual", + dtype: pa.DataType | None = None, + context: QueryEvaluationContext | None = None, +) -> pa.Field[Any]: + return with_column_expressions( + pa.field(name, dtype or pa.int64()), + ColumnExpressions(generated=expression(sql, context=context), generation=generation), + ) + + +def schema(*fields: pa.Field[Any]) -> SerializedSchema: + return SerializedSchema(pa.schema(fields).serialize().to_pybytes()) + + +def create(c: Client, token: AttachOpaqueData, columns: SerializedSchema, name: str = "items") -> None: + c.table_create(attach_opaque_data=token, schema_path=["main"], name=name, columns=columns) + + +def get( + c: Client, token: AttachOpaqueData, name: str = "items", tx: TransactionOpaqueData | None = None +) -> TableInfo | None: + return c.table_get(attach_opaque_data=token, schema_path=["main"], name=name, transaction_opaque_data=tx) + + +def columns( + c: Client, token: AttachOpaqueData, name: str = "items", tx: TransactionOpaqueData | None = None +) -> pa.Schema: + return pa.ipc.read_schema(pa.py_buffer(present(get(c, token, name, tx)).columns)) + + +def graph(c: Client, token: AttachOpaqueData, tx: TransactionOpaqueData | None = None) -> DependencySnapshot: + return c.catalog_dependencies(attach_opaque_data=token, transaction_opaque_data=tx) + + +@overload +def kwargs( + token: AttachOpaqueData, path: None = None, *, tx: TransactionOpaqueData | None = None +) -> CatalogObjectTransactionArgs: ... + + +@overload +def kwargs( + token: AttachOpaqueData, path: list[str], *, tx: TransactionOpaqueData | None = None +) -> CatalogColumnTransactionArgs: ... + + +def kwargs( + token: AttachOpaqueData, path: list[str] | None = None, *, tx: TransactionOpaqueData | None = None +) -> CatalogColumnTransactionArgs: + result: CatalogColumnTransactionArgs = dict( + attach_opaque_data=token, schema_path=["main"], name="items", transaction_opaque_data=tx + ) + if path is not None: + result["column_path"] = path + return result + + +def seed_generated(c: Client, token: AttachOpaqueData, *, views: bool = False) -> None: + create( + c, + token, + schema( + pa.field("id", pa.int64()), + generated("plus_one", "id + 1"), + generated("plus_two", "plus_one + 1"), + pa.field("keep", pa.string()), + ), + ) + if views: + c.view_create(attach_opaque_data=token, schema_path=["main"], name="v", definition="SELECT * FROM items") + c.view_create(attach_opaque_data=token, schema_path=["main"], name="v2", definition="SELECT * FROM v") + + +def test_semantic_view_guards_allow_comments_without_weakening_graph(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + create( + c, + token, + schema( + pa.field("a", pa.struct([pa.field("b", pa.int64()), pa.field("keep", pa.int64())])), + pa.field("keep", pa.int64()), + ), + ) + c.view_create(attach_opaque_data=token, schema_path=["main"], name="v", definition="SELECT * FROM items") + before = graph(c, token) + edges = before.edges() + protected = [ + edge for edge in edges if edge.dependent_ref().kind == "view" and edge.subject_ref().kind == "table" + ] + assert len(protected) == 1 and protected[0].blocks_alter and protected[0].subject_column_paths is None + for call in cast( + list[Callable[[], object]], + [ + lambda: c.table_rename(**kwargs(token), new_name="renamed"), + lambda: c.table_column_rename(**kwargs(token, ["a", "b"]), new_column_name="renamed"), + lambda: c.table_column_type_change( + **kwargs(token, ["a", "b"]), column_definition=schema(pa.field("b", pa.int32())) + ), + lambda: c.table_column_drop(**kwargs(token, ["a", "b"])), + ], + ): + with pytest.raises(CatalogClientError, match="depend|ALTER|alter"): + call() + assert graph(c, token) == before + c.table_comment_set(**kwargs(token), comment="harmless table label") + c.table_column_comment_set(**kwargs(token, ["a", "b"]), comment="harmless nested label") + assert graph(c, token).dependencies == before.dependencies + assert present(get(c, token)).comment == "harmless table label" + assert columns(c, token).field("a").type.field("b").metadata[b"comment"] == b"harmless nested label" + + +def test_raw_paths_and_table_rename_preserve_exact_graph_identity(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + create( + c, + token, + schema( + pa.field("a.b", pa.int64()), + pa.field("a", pa.struct([pa.field("b", pa.int64()), pa.field("keep", pa.int64())])), + pa.field("keep", pa.int64()), + ), + ) + c.table_column_rename(**kwargs(token, ["a", "b"]), new_column_name="renamed.b") + assert columns(c, token).field("a.b").type == pa.int64() + assert columns(c, token).field("a").type.names == ["renamed.b", "keep"] + c.table_column_type_change( + **kwargs(token, ["a", "renamed.b"]), column_definition=schema(pa.field("renamed.b", pa.string())) + ) + c.table_column_drop(**kwargs(token, ["a.b"])) + assert columns(c, token).names == ["a", "keep"] + c.table_rename(**kwargs(token), new_name="renamed.table") + assert get(c, token) is None + renamed = present(get(c, token, "renamed.table")) + assert renamed.name == "renamed.table" + names = {ref.name for ref in graph(c, token).object_refs() if ref.kind == "table"} + assert names == {"renamed.table"} + with client() as c: + assert get(c, token, "renamed.table") == renamed + + +def test_generated_edges_are_column_scoped_and_leaf_rename_updates_identity(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + seed_generated(c, token) + before = graph(c, token) + edges = [edge for edge in before.edges() if edge.dependent_column_path is not None] + assert { + (tuple(present(edge.dependent_column_path)), tuple(map(tuple, present(edge.subject_column_paths)))) + for edge in edges + } == {(("plus_one",), (("id",),)), (("plus_two",), (("plus_one",),))} + for path in [["id"], ["plus_one"]]: + with pytest.raises(CatalogClientError, match="depend|ALTER|alter"): + c.table_column_rename(**kwargs(token, path), new_column_name="changed") + with pytest.raises(CatalogClientError, match="depend|ALTER|alter"): + c.table_column_type_change( + **kwargs(token, path), column_definition=schema(pa.field(path[-1], pa.int32())) + ) + c.table_column_rename(**kwargs(token, ["plus_two"]), new_column_name="last") + current = graph(c, token) + assert any(edge.dependent_column_path == ["last"] for edge in current.edges()) + assert not any(edge.dependent_column_path == ["plus_two"] for edge in current.edges()) + c.table_column_drop(**kwargs(token, ["last"])) + assert columns(c, token).names == ["id", "plus_one", "keep"] + assert len([edge for edge in graph(c, token).edges() if edge.dependent_column_path]) == 1 + + +def test_dotted_generated_reference_does_not_protect_unrelated_nested_path(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + create( + c, + token, + schema( + pa.field("a.b", pa.int64()), + pa.field("a", pa.struct([pa.field("b", pa.int64()), pa.field("keep", pa.int64())])), + generated("derived", '"a.b" + 1'), + pa.field("keep", pa.string()), + ), + ) + edge = next(edge for edge in graph(c, token).edges() if edge.dependent_column_path) + assert edge.subject_column_paths == [["a.b"]] + c.table_column_rename(**kwargs(token, ["a", "b"]), new_column_name="different") + with pytest.raises(CatalogClientError, match="depend|ALTER|alter"): + c.table_column_rename(**kwargs(token, ["a.b"]), new_column_name="different") + + +def test_drop_cascade_removes_generated_columns_and_views_without_dropping_table(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + seed_generated(c, token, views=True) + before = graph(c, token) + with pytest.raises(CatalogClientError, match="depend|ALTER|alter"): + c.table_column_drop(**kwargs(token, ["id"])) + assert graph(c, token) == before + c.table_column_drop(**kwargs(token, ["id"]), cascade=True) + assert columns(c, token).names == ["keep"] + assert c.view_get(attach_opaque_data=token, schema_path=["main"], name="v") is None + assert c.view_get(attach_opaque_data=token, schema_path=["main"], name="v2") is None + current = graph(c, token) + assert current.catalog_version == before.catalog_version + 1 + assert not any(edge.dependent_column_path is not None for edge in current.edges()) + assert {ref.kind for ref in current.object_refs()} == {"schema", "table"} + with client() as c: + assert graph(c, token) == current + + +@pytest.mark.parametrize("failure", ["column_authorization", "view_authorization", "publication"]) +def test_denied_cascade_closure_or_publication_preserves_every_object( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch, failure: str +) -> None: + with client() as c: + token = attach(c) + seed_generated(c, token, views=True) + before, original = graph(c, token), get(c, token) + + def deny_column(self: DependencyCatalog, ref: CatalogObjectRef, path: list[str], action: str) -> None: + if path == ["plus_two"]: + raise ValueError("denied generated column") + + def deny_view(self: DependencyCatalog, ref: CatalogObjectRef) -> None: + if ref.name == "v2": + raise ValueError("denied view") + + def fail_publish(self: DependencyCatalog) -> None: + raise ValueError("denied publication") + + name, hook = { + "column_authorization": ("_authorize_alter", deny_column), + "view_authorization": ("_authorize_drop", deny_view), + "publication": ("_after_write", fail_publish), + }[failure] + monkeypatch.setattr(DependencyCatalog, name, hook) + with pytest.raises(CatalogClientError, match="denied"): + c.table_column_drop(**kwargs(token, ["id"]), cascade=True) + assert graph(c, token) == before and get(c, token) == original + assert c.view_get(attach_opaque_data=token, schema_path=["main"], name="v2") is not None + + +def test_semantic_transactions_reconnect_rollback_and_conflict_preserve_metadata(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + seed_generated(c, token, views=True) + before = graph(c, token) + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.table_column_drop(**kwargs(token, ["id"], tx=tx), cascade=True) + pending = graph(c, token, tx) + assert columns(c, token, tx=tx).names == ["keep"] + assert graph(c, token) == before + with client() as c: + assert graph(c, token, tx) == pending + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert graph(c, token) == before + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.table_column_drop(**kwargs(token, ["id"], tx=tx), cascade=True) + c.table_comment_set(**kwargs(token), comment="concurrent metadata") + with pytest.raises(CatalogClientError, match="transaction conflict"): + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert columns(c, token).names == ["id", "plus_one", "plus_two", "keep"] + assert present(get(c, token)).comment == "concurrent metadata" + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.table_column_drop(**kwargs(token, ["id"], tx=tx), cascade=True) + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert columns(c, token).names == ["keep"] + + +@pytest.mark.parametrize( + "bad", + [ + generated("bad", "missing + 1"), + generated("bad", "sum(id)"), + generated("bad", "random()"), + generated("bad", "(SELECT 1)"), + generated("bad", "id + 1", generation="stored"), + generated("bad", "id + 1", context=QueryEvaluationContext(timezone="Europe/London")), + generated("bad", "id + 1", dtype=pa.string()), + ], +) +def test_unsupported_generated_semantics_do_not_publish_incomplete_dependencies( + backend: DependencyWorker, bad: pa.Field[Any] +) -> None: + with client() as c: + token = attach(c) + before = graph(c, token) + with pytest.raises(CatalogClientError): + create(c, token, schema(pa.field("id", pa.int64()), bad)) + assert get(c, token) is None and graph(c, token) == before + + +def test_generated_cycles_and_last_column_cascade_are_atomic(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + before = graph(c, token) + with pytest.raises(CatalogClientError, match="cycle"): + create(c, token, schema(generated("a", "b + 1"), generated("b", "a + 1"))) + assert graph(c, token) == before + create(c, token, schema(pa.field("id", pa.int64()), generated("derived", "id + 1"))) + before = graph(c, token) + with pytest.raises(CatalogClientError, match="last"): + c.table_column_drop(**kwargs(token, ["id"]), cascade=True) + assert graph(c, token) == before + + +def test_generated_case_aliases_share_one_resolved_subject_path(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + create(c, token, schema(pa.field("a", pa.int64()), generated("both", "a + A"))) + edges = [edge for edge in graph(c, token).edges() if edge.dependent_column_path] + assert len(edges) == 1 + assert edges[0].dependent_column_path == ["both"] + assert edges[0].subject_column_paths == [["a"]] + with pytest.raises(CatalogClientError, match="dependent"): + c.table_column_drop(**kwargs(token, ["a"])) + + +def test_table_create_rejects_schema_trailing_bytes_without_publication(backend: DependencyWorker) -> None: + with client() as c: + token = attach(c) + before = graph(c, token) + with pytest.raises(CatalogClientError, match="trailing"): + create(c, token, SerializedSchema(schema(pa.field("a", pa.int64())) + b"unexpected-suffix")) + assert get(c, token) is None + assert graph(c, token) == before diff --git a/tests/catalog/test_dependency_alter_negotiation.py b/tests/catalog/test_dependency_alter_negotiation.py new file mode 100644 index 00000000..338765ac --- /dev/null +++ b/tests/catalog/test_dependency_alter_negotiation.py @@ -0,0 +1,206 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Mandatory column expressions preserve existing transaction promises.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import replace + +import pyarrow as pa +import pytest + +from vgi.catalog import OnConflict, TableInfo +from vgi.catalog.capabilities import CATALOG_DEPENDENCIES_READ, ROW_SCALAR_EXPRESSION, TRANSACTION_DDL +from vgi.catalog.catalog_interface import AttachOpaqueData, SerializedSchema, TransactionOpaqueData +from vgi.catalog.sql import ColumnExpressions, with_column_expressions +from vgi.exceptions import CatalogCapabilityError +from vgi.protocol import TableCreateRequest + +from .dependencies_reference import DEPENDENCY_PROFILES, DependencyCatalog, DependencyWorker +from .test_dependencies_protocol import OFFER, request +from .test_dependencies_protocol import backend as backend +from .test_dependency_alter_lifecycle import expression, generated, schema +from .typing_helpers import CatalogColumnTransactionArgs, CatalogObjectTransactionArgs, present + + +def create_request( + token: AttachOpaqueData, columns: SerializedSchema, tx: TransactionOpaqueData | None = None +) -> TableCreateRequest: + return TableCreateRequest( + attach_opaque_data=token, + schema_path=["main"], + name="items", + columns=columns, + on_conflict=OnConflict.ERROR, + constraint_definitions=[], + transaction_opaque_data=tx, + ) + + +def limited_expression_token(backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch, mode: str) -> AttachOpaqueData: + profiles = replace(DEPENDENCY_PROFILES, read=[p for p in DEPENDENCY_PROFILES.read if p != ROW_SCALAR_EXPRESSION]) + token = backend.catalog_attach(request(replace(OFFER, catalog=profiles) if mode == "unselected" else OFFER)) + if mode == "withdrawn": + monkeypatch.setattr(DependencyCatalog, "profiles", profiles) + return token.attach_opaque_data + + +@pytest.mark.parametrize("mode", ["unselected", "withdrawn"]) +@pytest.mark.parametrize("operation", ["create", "add"]) +@pytest.mark.parametrize("nested", [False, True]) +def test_canonical_generated_representation_needs_no_expression_read_opt_in( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch, mode: str, operation: str, nested: bool +) -> None: + token = limited_expression_token(backend, monkeypatch, mode) + field = generated("computed", "id + 1") + if nested: + field = pa.field("nested", pa.struct([field])) + calls = [] + monkeypatch.setattr(backend._get_catalog(), "table_create", lambda **kwargs: calls.append(kwargs)) + monkeypatch.setattr(backend._get_catalog(), "table_column_add", lambda **kwargs: calls.append(kwargs)) + before = backend.catalog_dependencies(token) + if operation == "create": + backend.catalog_table_create(create_request(token, schema(pa.field("id", pa.int64()), field))) + else: + backend.catalog_table_column_add(token, ["main"], "items", [field.name], schema(field)) + assert len(calls) == 1 and backend.catalog_dependencies(token) == before + + +@pytest.mark.parametrize("mode", ["unselected", "withdrawn"]) +def test_all_table_discovery_paths_preserve_generated_without_read_opt_in( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch, mode: str +) -> None: + full = backend.catalog_attach(request()).attach_opaque_data + backend.catalog_table_create( + create_request(full, schema(pa.field("id", pa.int64()), generated("computed", "id + 1"))) + ) + token = limited_expression_token(backend, monkeypatch, mode) + before = backend.catalog_dependencies(token) + reads: list[Callable[[], object]] = [ + lambda: backend.catalog_table_get(token, ["main"], "items"), + lambda: backend.catalog_schema_contents_tables(token, ["main"]), + lambda: backend.catalog_contents(token), + ] + for read in reads: + read() + assert backend.catalog_dependencies(token) == before + + +def test_default_and_nested_expression_readback_is_mandatory_without_profile_negotiation( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + field = with_column_expressions(pa.field("child", pa.int64()), ColumnExpressions(default=expression("42"))) + columns = schema(pa.field("parent", pa.struct([field]))) + info = TableInfo(name="items", schema_path=["main"], columns=columns, constraints=[], comment=None, tags={}) + monkeypatch.setattr(backend._get_catalog(), "table_get", lambda **kwargs: info) + full = backend.catalog_attach(request()).attach_opaque_data + result = backend.catalog_table_get(full, ["main"], "items") + assert TableInfo.deserialize_from_bytes(result.items[0]).columns == columns + limited = limited_expression_token(backend, monkeypatch, "unselected") + result = backend.catalog_table_get(limited, ["main"], "items") + assert TableInfo.deserialize_from_bytes(result.items[0]).columns == columns + + +@pytest.mark.parametrize("mode", ["unselected", "omit_both", "withdrawn"]) +def test_dependency_mutations_require_selected_current_transaction_ddl_before_effects( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch, mode: str +) -> None: + profiles = replace( + DEPENDENCY_PROFILES, + transaction=[], + read=[p for p in DEPENDENCY_PROFILES.read if mode != "omit_both" or p != CATALOG_DEPENDENCIES_READ], + ) + offer = OFFER if mode == "withdrawn" else replace(OFFER, catalog=profiles) + token = backend.catalog_attach(request(offer)).attach_opaque_data + tx = TransactionOpaqueData(present(backend.catalog_transaction_begin(token).transaction_opaque_data)) + if mode == "withdrawn": + monkeypatch.setattr(DependencyCatalog, "profiles", profiles) + args: CatalogObjectTransactionArgs = dict( + attach_opaque_data=token, schema_path=["main"], name="items", transaction_opaque_data=tx + ) + column: CatalogColumnTransactionArgs = {**args, "column_path": ["id"]} + methods: dict[str, Callable[[], object]] = { + "schema_create": lambda: backend.catalog_schema_create(token, ["new"], transaction_opaque_data=tx), + "schema_drop": lambda: backend.catalog_schema_drop(token, ["new"], transaction_opaque_data=tx), + "table_create": lambda: backend.catalog_table_create( + create_request(token, schema(pa.field("id", pa.int64())), tx) + ), + "table_drop": lambda: backend.catalog_table_drop(**args), + "table_rename": lambda: backend.catalog_table_rename(**args, new_name="renamed"), + "table_comment_set": lambda: backend.catalog_table_comment_set(**args, comment="comment"), + "table_column_comment_set": lambda: backend.catalog_table_column_comment_set(**column, comment="comment"), + "table_column_add": lambda: backend.catalog_table_column_add( + **column, column_definition=schema(pa.field("id", pa.int64())) + ), + "table_column_drop": lambda: backend.catalog_table_column_drop(**column, cascade=True), + "table_column_rename": lambda: backend.catalog_table_column_rename(**column, new_column_name="renamed"), + "table_column_type_change": lambda: backend.catalog_table_column_type_change( + **column, column_definition=schema(pa.field("id", pa.int32())) + ), + "table_column_default_set": lambda: backend.catalog_table_column_default_set( + **column, expression=expression("42").serialize_to_bytes() + ), + "table_column_default_drop": lambda: backend.catalog_table_column_default_drop(**column), + "view_create": lambda: backend.catalog_view_create( + **args, definition="SELECT * FROM source", on_conflict=OnConflict.ERROR + ), + "view_drop": lambda: backend.catalog_view_drop(**args), + "view_rename": lambda: backend.catalog_view_rename(**args, new_name="renamed"), + } + calls = [] + for name in methods: + monkeypatch.setattr(backend._get_catalog(), name, lambda **kwargs: calls.append(kwargs)) + version = backend.catalog_version(token).version + for mutation in methods.values(): + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + mutation() + assert calls == [] and backend.catalog_version(token).version == version + backend.catalog_transaction_rollback(token, tx) + + +def test_legacy_expression_metadata_rejects_but_plain_non_dependency_transactions_remain( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + profiles = replace(DEPENDENCY_PROFILES, read=[], transaction=[]) + monkeypatch.setattr(DependencyCatalog, "profiles", profiles) + token = backend.catalog_attach(request(None)).attach_opaque_data + tx = TransactionOpaqueData(present(backend.catalog_transaction_begin(token).transaction_opaque_data)) + calls = [] + monkeypatch.setattr(backend._get_catalog(), "table_create", lambda **kwargs: calls.append(kwargs)) + legacy = schema(pa.field("id", pa.int64(), metadata={b"default": b"42"})) + with pytest.raises(ValueError, match="canonical SqlExpression"): + backend.catalog_table_create(create_request(token, legacy, tx)) + assert calls == [] + plain = schema(pa.field("id", pa.int64())) + backend.catalog_table_create(create_request(token, plain, tx)) + assert len(calls) == 1 and calls[0]["columns"] == plain + backend.catalog_transaction_rollback(token, tx) + + +@pytest.mark.parametrize("operation", ["create", "add"]) +def test_canonical_expression_ddl_requires_transaction_profile_without_graph_profile( + backend: DependencyWorker, monkeypatch: pytest.MonkeyPatch, operation: str +) -> None: + profiles = replace( + DEPENDENCY_PROFILES, + read=[p for p in DEPENDENCY_PROFILES.read if p != CATALOG_DEPENDENCIES_READ], + transaction=[], + ) + monkeypatch.setattr(DependencyCatalog, "profiles", profiles) + token = backend.catalog_attach(request(replace(OFFER, catalog=profiles))).attach_opaque_data + tx = TransactionOpaqueData(present(backend.catalog_transaction_begin(token).transaction_opaque_data)) + calls = [] + monkeypatch.setattr(backend._get_catalog(), "table_create", lambda **kwargs: calls.append(kwargs)) + monkeypatch.setattr(backend._get_catalog(), "table_column_add", lambda **kwargs: calls.append(kwargs)) + field = generated("computed", "id + 1") + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + if operation == "create": + backend.catalog_table_create(create_request(token, schema(pa.field("id", pa.int64()), field), tx)) + else: + backend.catalog_table_column_add( + token, ["main"], "items", ["computed"], schema(field), transaction_opaque_data=tx + ) + assert calls == [] + backend.catalog_transaction_rollback(token, tx) diff --git a/tests/catalog/test_example_worker_catalog.py b/tests/catalog/test_example_worker_catalog.py index 5defebd3..5507a1dc 100644 --- a/tests/catalog/test_example_worker_catalog.py +++ b/tests/catalog/test_example_worker_catalog.py @@ -21,7 +21,11 @@ TableInfo, ViewInfo, ) +from vgi.catalog.capabilities import INDEX_DEFINITION_READ, CatalogCapabilities +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.macros import legacy_macro_fields from vgi.client import Client +from vgi.protocol import ClientCapabilities # Worker command for catalog tests EXAMPLE_WORKER = FIXTURE_WORKER @@ -419,7 +423,7 @@ class TestExampleWorkerIndexes: """Test ExampleWorker's index catalog entries.""" def test_schema_contents_returns_indexes_in_data(self) -> None: - """`data` has 2 real indexes on `numbers`. + """`data` has two metadata-only index declarations for `numbers`. Regression: `type=SchemaObjectType.INDEX` used to silently fall through to the functions RPC (`catalog_schema_contents_functions`) @@ -427,7 +431,18 @@ def test_schema_contents_returns_indexes_in_data(self) -> None: """ client = Client(EXAMPLE_WORKER) attach_result = client.catalog_attach( - name="example", options={}, data_version_spec=None, implementation_version=None + name="example", + options={}, + data_version_spec=None, + implementation_version=None, + client_capabilities=ClientCapabilities( + engine="test", + native_formats=[], + catalogs=[], + can_stream=False, + filter_encodings=[], + catalog=CatalogCapabilities(read=[INDEX_DEFINITION_READ]), + ), ) indexes = list( @@ -442,8 +457,9 @@ def test_schema_contents_returns_indexes_in_data(self) -> None: assert index_names == {"idx_numbers_value", "idx_numbers_value_unique"} for index in indexes: assert isinstance(index, IndexInfo) - assert index.table_name == "numbers" - assert index.expressions == ["value"] + assert index.definition.table.name == "numbers" + assert index.definition.key_definitions()[0].column_path == ["value"] + assert index.definition.algorithm == "fixture.metadata" class TestExampleWorkerMacros: @@ -492,7 +508,7 @@ def test_macro_get_returns_correct_info(self) -> None: assert isinstance(info, MacroInfo) assert info.name == "vgi_multiply" assert info.schema_path == ["main"] - assert info.definition == "x * y" + assert info.overloads[0].definition == "x * y" assert info.comment == "Multiply two values" def test_macro_info_has_correct_types(self) -> None: @@ -533,7 +549,7 @@ def test_macro_info_has_parameters(self) -> None: ) assert info is not None - assert info.parameters == ["x", "y"] + assert [p.name for p in info.overloads[0].parameters] == ["x", "y"] def test_macro_info_has_parameter_default_values(self) -> None: """RecordBatch parameter defaults survive round-trip with types.""" @@ -549,9 +565,14 @@ def test_macro_info_has_parameter_default_values(self) -> None: ) assert info is not None - assert info.parameters == ["val", "lo", "hi"] - assert info.parameter_default_values is not None - defaults = info.parameter_default_values + assert [p.name for p in info.overloads[0].parameters] == ["val", "lo", "hi"] + assert ( + legacy_macro_fields(info.overloads, evaluation_context=QueryEvaluationContext())["parameter_default_values"] + is not None + ) + defaults = legacy_macro_fields(info.overloads, evaluation_context=QueryEvaluationContext())[ + "parameter_default_values" + ] assert defaults.num_rows == 1 assert set(defaults.schema.names) == {"lo", "hi"} assert defaults.column("lo")[0].as_py() == 0 diff --git a/tests/catalog/test_expression_contract.py b/tests/catalog/test_expression_contract.py new file mode 100644 index 00000000..7bd1b0d9 --- /dev/null +++ b/tests/catalog/test_expression_contract.py @@ -0,0 +1,179 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Required contexts, portable column metadata and bounded deterministic role proof.""" + +from __future__ import annotations + +import base64 +from collections.abc import Callable +from dataclasses import replace +from typing import Any, cast + +import pyarrow as pa +import pytest + +from vgi.catalog import sql as sql_module +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.macros import MACRO_DEFAULT_PROFILE, MACRO_PROFILE, MacroOverload, MacroParameter +from vgi.catalog.query import QueryEvaluationContext as QueryContextCompatibilityImport +from vgi.catalog.sql import ( + DEFAULT_EXPRESSION_KEY, + GENERATED_EXPRESSION_KEY, + GENERATION_KEY, + ColumnExpressions, + SqlExpression, + SqlStatement, + column_expressions, + with_column_expressions, +) + +from .expression_reference import evaluate_expression, evaluate_generated + +OWNER = ["a.b", "nested"] +CONTEXT = QueryEvaluationContext() + + +def expression(sql: str, **changes: Any) -> SqlExpression: + return SqlExpression( + sql=sql, + profile=ROW_SCALAR_EXPRESSION, + schema_path=OWNER, + evaluation_context=CONTEXT, + **changes, + ) + + +def test_context_is_required_and_the_query_identity_and_digest_are_unchanged() -> None: + assert QueryContextCompatibilityImport is QueryEvaluationContext + assert CONTEXT.digest("duckdb.sql.v1", "vgi.remote.readonly.v1").hex() == ( + "0d33dfb516d67c8096fb174f0ae08a04f028ad5669e4416cde2973a2dd352d34" + ) + for record in [SqlExpression, SqlStatement]: + with pytest.raises(TypeError, match="evaluation_context"): + cast("Callable[..., SqlExpression | SqlStatement]", record)( + sql="42", profile="duckdb.scalar.v1", schema_path=OWNER + ) + definition = record(sql="42", profile="duckdb.scalar.v1", schema_path=OWNER, evaluation_context=CONTEXT) + assert record.deserialize_from_bytes(definition.serialize_to_bytes()) == definition + with pytest.raises(ValueError, match="trailing"): + record.deserialize_from_bytes(definition.serialize_to_bytes() + b"trailing") + with pa.ipc.open_stream(definition.serialize_to_bytes()) as reader: + batch = reader.read_next_batch() + with pytest.raises(ValueError, match="complete schema"): + record.deserialize_from_batch(batch.select(["sql", "profile", "schema_path"])) + + +def test_macro_default_owner_and_context_must_equal_its_body_context() -> None: + default = replace(expression("42"), profile=MACRO_DEFAULT_PROFILE) + overload = MacroOverload( + parameters=[MacroParameter(name="x", default_expression=default)], + definition="x + 1", + profile=MACRO_PROFILE, + schema_path=OWNER, + macro_type="scalar", + evaluation_context=CONTEXT, + ) + assert MacroOverload.deserialize_from_bytes(overload.serialize_to_bytes()) == overload + with pytest.raises(ValueError, match="evaluation context"): + replace(overload, evaluation_context=replace(CONTEXT, timezone="Europe/Paris")) + with pytest.raises(ValueError, match="owner schema"): + replace(overload, schema_path=["other"]) + + +def test_column_expression_metadata_preserves_base64_ipc_and_unrelated_metadata() -> None: + field = pa.field("a.b", pa.int64(), metadata={b"custom": "caf\u00e9".encode(), b"comment": b""}) + typed_null = ColumnExpressions(default=expression("CAST(NULL AS BIGINT)")) + encoded = with_column_expressions(field, typed_null) + restored = pa.ipc.read_schema(pa.py_buffer(pa.schema([encoded]).serialize())).field(0) + assert restored.equals(encoded, check_metadata=True) + assert column_expressions(restored) == typed_null + assert restored.metadata is not None + assert restored.metadata[b"custom"] == "caf\u00e9".encode() + assert restored.metadata[DEFAULT_EXPRESSION_KEY].isascii() + assert typed_null.default is not None + assert base64.b64decode(restored.metadata[DEFAULT_EXPRESSION_KEY], validate=True) == ( + typed_null.default.serialize_to_bytes() + ) + assert column_expressions(field).default is None + assert with_column_expressions(encoded, ColumnExpressions()).equals(field, check_metadata=True) + generated = ColumnExpressions(generated=expression('"a.b" * 2'), generation="virtual") + assert column_expressions(with_column_expressions(field, generated)) == generated + + +def test_column_expression_metadata_rejects_collisions_unknown_versions_and_bad_unions() -> None: + raw_payload = expression("42").serialize_to_bytes() + payload = base64.b64encode(raw_payload) + for metadata in [ + {DEFAULT_EXPRESSION_KEY: payload, b"default": b"42"}, + {GENERATED_EXPRESSION_KEY: payload}, + {GENERATION_KEY: b"virtual"}, + {GENERATED_EXPRESSION_KEY: payload, GENERATION_KEY: b"mystery"}, + {DEFAULT_EXPRESSION_KEY: payload, GENERATED_EXPRESSION_KEY: payload, GENERATION_KEY: b"virtual"}, + {b"vgi.column.default.v2": payload}, + {DEFAULT_EXPRESSION_KEY: base64.b64encode(raw_payload + b"extra")}, + cast("Callable[..., pa.KeyValueMetadata]", pa.KeyValueMetadata)( + [(DEFAULT_EXPRESSION_KEY, payload), (DEFAULT_EXPRESSION_KEY, payload)] + ), + ]: + with pytest.raises(ValueError): + column_expressions(cast("Callable[..., pa.Field[Any]]", pa.field)("x", pa.int64(), metadata=metadata)) + + +@pytest.mark.parametrize("payload", [b"AA", b"AA==\n", b"AAAA====", b"AB==", b"AA-_", b"!!!!", b"\xff"]) +def test_column_expression_metadata_rejects_noncanonical_base64(payload: bytes) -> None: + with pytest.raises(ValueError, match="canonical padded base64"): + column_expressions(pa.field("x", pa.int64(), metadata={DEFAULT_EXPRESSION_KEY: payload})) + + +def test_column_expression_metadata_rejects_raw_ipc_and_bounds_both_encodings(monkeypatch: pytest.MonkeyPatch) -> None: + raw_payload = expression("42").serialize_to_bytes() + with pytest.raises(ValueError, match="canonical padded base64"): + column_expressions(pa.field("x", pa.int64(), metadata={DEFAULT_EXPRESSION_KEY: raw_payload})) + monkeypatch.setattr(sql_module, "MAX_SQL_RECORD_BYTES", 1) + # The first exceeds the encoded bound; the second fits that bound but + # decodes to two bytes, exceeding the decoded bound of one byte. + for payload in [base64.b64encode(raw_payload), b"AAA="]: + with pytest.raises(ValueError, match="metadata size"): + column_expressions(pa.field("x", pa.int64(), metadata={DEFAULT_EXPRESSION_KEY: payload})) + + +def test_check_preserves_true_false_and_null_and_default_has_no_row_scope() -> None: + table = pa.table({"n": pa.array([-1, 0, None], type=pa.int64())}) + assert evaluate_expression(expression("n >= 0"), table, OWNER, role="check").to_pylist() == [False, True, None] + assert evaluate_expression(expression("40 + 2"), table, OWNER, role="default").to_pylist() == [42, 42, 42] + with pytest.raises(ValueError, match="column reference"): + evaluate_expression(expression("n + 1"), table, OWNER, role="default") + assert evaluate_expression(expression("CAST(n AS INTEGER)"), table, OWNER, role="using").to_pylist() == [ + -1, + 0, + None, + ] + + +def test_generated_dependencies_bind_in_order_and_cycles_reject_before_publication() -> None: + table = pa.table({"n": pa.array([2, None], type=pa.int64())}) + generated = evaluate_generated(table, {"last": expression("twice + 1"), "twice": expression("n * 2")}, OWNER) + assert generated["last"].to_pylist() == [5, None] + assert table.schema.names == ["n"] + with pytest.raises(ValueError, match="cycle"): + evaluate_generated(table, {"x": expression("y + 1"), "y": expression("x + 1")}, OWNER) + + +@pytest.mark.parametrize("sql", ["random()", "(SELECT 1)", "$value", "sum(n)", "n OVER ()", "main.abs(n)"]) +def test_unqualified_expression_forms_reject(sql: str) -> None: + with pytest.raises(ValueError): + evaluate_expression(expression(sql), pa.table({"n": [1]}), OWNER, role="generated") + + +def test_reference_rejects_unqualified_context_and_type_without_implicit_fallback() -> None: + with pytest.raises(ValueError, match="context"): + evaluate_expression( + replace(expression("n"), evaluation_context=replace(CONTEXT, collation="nocase")), + pa.table({"n": [1]}), + OWNER, + role="using", + ) + with pytest.raises(ValueError, match="primitive"): + evaluate_expression(expression("n"), pa.table({"n": [1.5]}), OWNER, role="using") diff --git a/tests/catalog/test_generated_columns_protocol.py b/tests/catalog/test_generated_columns_protocol.py new file mode 100644 index 00000000..5fc59a28 --- /dev/null +++ b/tests/catalog/test_generated_columns_protocol.py @@ -0,0 +1,318 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Canonical VIRTUAL creation, durable physical scans and rejection boundaries.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import replace +from pathlib import Path +from typing import Any, Literal + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.arguments import Arguments +from vgi.catalog import OnConflict, Table, TableInfo +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.catalog_interface import AttachOpaqueData, SerializedSchema, TransactionOpaqueData +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import ColumnExpressions, SqlExpression, column_expressions, with_column_expressions +from vgi.client import Client +from vgi.client.catalog_mixin import CatalogClientError +from vgi.protocol import ClientCapabilities +from vgi.serve import create_app +from vgi.worker import Worker + +from .dependencies_reference import DEPENDENCY_PROFILES +from .generated_columns_reference import GeneratedColumnsCatalog, GeneratedColumnsWorker +from .typing_helpers import CatalogObjectTransactionArgs, present + +KEY = b"generated-reference-signing-key" +OFFER = ClientCapabilities( + engine="reference", + native_formats=[], + catalogs=[], + can_stream=False, + filter_encodings=[], + catalog=replace(DEPENDENCY_PROFILES, read=[p for p in DEPENDENCY_PROFILES.read if p != ROW_SCALAR_EXPRESSION]), +) + + +def expression( + sql: str, *, owner: list[str] | None = None, context: QueryEvaluationContext | None = None +) -> SqlExpression: + return SqlExpression( + sql=sql, + profile=ROW_SCALAR_EXPRESSION, + schema_path=owner or ["main"], + evaluation_context=context or QueryEvaluationContext(), + ) + + +def generated( + name: str, + sql: str, + *, + dtype: pa.DataType | None = None, + generation: Literal["virtual", "stored"] = "virtual", + owner: list[str] | None = None, + context: QueryEvaluationContext | None = None, +) -> pa.Field[Any]: + return with_column_expressions( + pa.field(name, dtype or pa.int64()), + ColumnExpressions(generated=expression(sql, owner=owner, context=context), generation=generation), + ) + + +def columns() -> pa.Schema: + return pa.schema( + [ + generated("doubled", "n * 2"), + pa.field("n", pa.int64(), metadata={b"comment": b"physical"}), + generated("label", "'item_' || CAST(n AS VARCHAR)", dtype=pa.string()), + pa.field("text", pa.string()), + generated("after", "doubled + 1"), + ] + ) + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> GeneratedColumnsWorker: + monkeypatch.setattr(GeneratedColumnsCatalog, "database_path", str(tmp_path / "generated.sqlite")) + worker = GeneratedColumnsWorker(quiet=True) + worker._signing_key = KEY + return worker + + +def client(token: AttachOpaqueData | None = None) -> Client: + return Client.from_http( + "http://vgi.test", + attach_opaque_data=token, + httpx_client=_SyncTestClient(create_app(GeneratedColumnsWorker, describe=False, signing_key=KEY)), + ) + + +def attach(c: Client, offer: ClientCapabilities | None = OFFER) -> AttachOpaqueData: + return c.catalog_attach( + name="generated_columns", data_version_spec=None, implementation_version=None, client_capabilities=offer + ).attach_opaque_data + + +def args( + token: AttachOpaqueData, name: str = "items", tx: TransactionOpaqueData | None = None +) -> CatalogObjectTransactionArgs: + return dict(attach_opaque_data=token, schema_path=["main"], name=name, transaction_opaque_data=tx) + + +def seed( + backend: GeneratedColumnsWorker, + token: AttachOpaqueData, + rows: list[dict[str, Any]], + name: str = "items", + tx: TransactionOpaqueData | None = None, +) -> None: + catalog = backend._get_catalog() + assert isinstance(catalog, GeneratedColumnsCatalog) + catalog.seed_rows( + **args(backend._unwrap_attach(token), name, backend._unwrap_transaction(tx, token) if tx else None), rows=rows + ) + + +def scan(c: Client, token: AttachOpaqueData, name: str = "items", tx: TransactionOpaqueData | None = None) -> pa.Table: + function = c.table_scan_function_get(**args(token, name, tx)) + batches = list( + c.table_function( + function_name=function.function_name, + schema_path=["main"], + arguments=Arguments(positional=tuple(function.positional_arguments), named=function.named_arguments), + transaction_opaque_data=tx, + ) + ) + return pa.Table.from_batches(batches) + + +def test_convenience_generated_columns_use_explicit_owner_and_canonical_virtual_metadata() -> None: + table = Table( + name="t", + columns=pa.schema([pa.field("n", pa.int64()), pa.field("g", pa.int64())]), + generated_columns={"g": "n * 2"}, + ) + for owner in [["a.b"], ["a", "b"]]: + schema = pa.ipc.read_schema(pa.py_buffer(table.to_table_info(owner).columns)) + expr = column_expressions(schema.field("g")) + assert expr == ColumnExpressions(generated=expression("n * 2", owner=owner), generation="virtual") + assert b"generated_expression" not in (schema.field("g").metadata or {}) + conflict = with_column_expressions(pa.field("g", pa.int64()), ColumnExpressions(default=expression("1"))) + with pytest.raises(ValueError, match="cannot also have a default"): + Table(name="t", columns=pa.schema([conflict]), generated_columns={"g": "2"}).to_table_info(["main"]) + + +@pytest.mark.parametrize( + "field,error,message", + [ + (generated("g", "n + 1", generation="stored"), NotImplementedError, "stored"), + (pa.field("g", pa.int64(), metadata={b"generated_expression": b"n + 1"}), ValueError, "canonical"), + ( + pa.field("nested", pa.struct([pa.field("g", pa.int64(), metadata={b"generated_expression": b"n + 1"})])), + ValueError, + "canonical", + ), + ], +) +def test_unsupported_generation_rejects_before_provider_and_during_discovery( + backend: GeneratedColumnsWorker, + monkeypatch: pytest.MonkeyPatch, + field: pa.Field[Any], + error: type[Exception], + message: str, +) -> None: + from vgi.catalog.capabilities import CatalogCapabilities + from vgi.protocol import TableCreateRequest + + schema = pa.schema([pa.field("n", pa.int64()), field]) + calls = [] + monkeypatch.setattr(backend._get_catalog(), "table_create", lambda **kwargs: calls.append(kwargs)) + with client() as c: + token = attach(c, None) + with pytest.raises(error, match=message): + backend.catalog_table_create( + TableCreateRequest( + **args(token), + columns=SerializedSchema(schema.serialize().to_pybytes()), + on_conflict=OnConflict.ERROR, + constraint_definitions=[], + ) + ) + assert calls == [] + with pytest.raises(error, match=message): + Worker._require_column_expression_profiles(schema, CatalogCapabilities(), "read", ["main"]) + info = TableInfo( + name="items", + schema_path=["main"], + columns=SerializedSchema(schema.serialize().to_pybytes()), + constraints=[], + comment=None, + tags={}, + ) + monkeypatch.setattr(backend._get_catalog(), "table_get", lambda **kwargs: info) + with pytest.raises(error, match=message): + backend.catalog_table_get(**args(token)) + + +def test_signed_http_generated_create_scan_reconnect_and_dependency_readback(backend: GeneratedColumnsWorker) -> None: + schema = columns() + data: list[dict[str, Any]] = [{"n": 3, "text": "first"}, {"n": None, "text": "null"}] + with client() as c: + token = attach(c) + c.table_create(**args(token), columns=SerializedSchema(schema.serialize().to_pybytes())) + seed(backend, token, data) + info = present(c.table_get(**args(token))) + assert pa.ipc.read_schema(pa.py_buffer(info.columns)).equals(schema, check_metadata=True) + graph = c.catalog_dependencies(attach_opaque_data=token) + edges = [edge for edge in graph.edges() if edge.dependent_column_path] + assert {tuple(present(edge.dependent_column_path)) for edge in edges} == {("doubled",), ("label",), ("after",)} + with client(token) as c: + physical = scan(c, token) + assert physical.schema.names == ["n", "text"] + assert physical.to_pylist() == data + assert all(column_expressions(field) == ColumnExpressions() for field in physical.schema) + assert present(c.table_get(**args(token))).columns == info.columns + assert c.catalog_dependencies(attach_opaque_data=token) == graph + # Representation remains readable even without any catalog capability offer. + with client() as c: + unaware = attach(c, None) + assert present(c.table_get(**args(unaware))).columns == info.columns + + +def test_generated_create_rollback_and_commit_survive_reconnect(backend: GeneratedColumnsWorker) -> None: + with client() as c: + token = attach(c) + tx = present(c.catalog_transaction_begin(attach_opaque_data=token)) + c.table_create(**args(token, tx=tx), columns=SerializedSchema(columns().serialize().to_pybytes())) + seed(backend, token, [{"n": 7, "text": "pending"}], tx=tx) + assert c.table_get(**args(token)) is None + with client(token) as c: + assert scan(c, token, tx=tx).to_pylist() == [{"n": 7, "text": "pending"}] + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=tx) + assert c.table_get(**args(token)) is None + tx = present(c.catalog_transaction_begin(attach_opaque_data=token)) + c.table_create(**args(token, tx=tx), columns=SerializedSchema(columns().serialize().to_pybytes())) + seed(backend, token, [{"n": 9, "text": "committed"}], tx=tx) + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=tx) + with client(token) as c: + assert scan(c, token).to_pylist() == [{"n": 9, "text": "committed"}] + + +@pytest.mark.parametrize( + "bad", + [ + generated("g", "missing + 1"), + generated("g", "random()"), + generated("g", "sum(n)"), + generated("g", "(SELECT 1)"), + generated("g", "n + 1", dtype=pa.string()), + generated("g", "n + 1", generation="stored"), + generated("g", "n + 1", owner=["elsewhere"]), + generated("g", "n + 1", context=QueryEvaluationContext(timezone="Europe/London")), + generated("g", "row_number() OVER ()"), + generated("g", "main.abs(n)"), + generated("g", "n) FROM input_data; SELECT (1"), + ], +) +def test_invalid_generated_replace_preserves_definition_rows_and_version( + backend: GeneratedColumnsWorker, bad: pa.Field[Any] +) -> None: + with client() as c: + token = attach(c) + c.table_create(**args(token), columns=SerializedSchema(columns().serialize().to_pybytes())) + seed(backend, token, [{"n": 4, "text": "kept"}]) + before = c.table_get(**args(token)) + version = c.catalog_version(attach_opaque_data=token) + with pytest.raises(CatalogClientError): + c.table_create( + **args(token), + columns=SerializedSchema(pa.schema([pa.field("n", pa.int64()), bad]).serialize().to_pybytes()), + on_conflict=OnConflict.REPLACE, + ) + assert c.table_get(**args(token)) == before + assert c.catalog_version(attach_opaque_data=token) == version + with client(token) as c: + assert scan(c, token).to_pylist() == [{"n": 4, "text": "kept"}] + + +def test_cycles_and_unsupported_alter_reject_without_changes(backend: GeneratedColumnsWorker) -> None: + with client() as c: + token = attach(c) + version = c.catalog_version(attach_opaque_data=token) + cycle = pa.schema([pa.field("n", pa.int64()), generated("a", "b + 1"), generated("b", "a + 1")]) + with pytest.raises(CatalogClientError, match="cycle"): + c.table_create(**args(token), columns=SerializedSchema(cycle.serialize().to_pybytes())) + assert c.catalog_version(attach_opaque_data=token) == version + c.table_create(**args(token), columns=SerializedSchema(columns().serialize().to_pybytes())) + seed(backend, token, [{"n": 1, "text": "kept"}]) + before = c.table_get(**args(token)) + version = c.catalog_version(attach_opaque_data=token) + operations: list[Callable[[], None]] = [ + lambda: c.table_column_rename(**args(token), column_path=["n"], new_column_name="renamed"), + lambda: c.table_column_drop(**args(token), column_path=["n"], cascade=True), + lambda: c.table_column_type_change( + **args(token), + column_path=["n"], + column_definition=SerializedSchema(pa.schema([pa.field("n", pa.int32())]).serialize().to_pybytes()), + ), + lambda: c.table_column_add( + **args(token), + column_path=["extra"], + column_definition=SerializedSchema(pa.schema([generated("extra", "n + 1")]).serialize().to_pybytes()), + ), + ] + for operation in operations: + with pytest.raises(CatalogClientError): + operation() + assert c.table_get(**args(token)) == before + assert c.catalog_version(attach_opaque_data=token) == version + with client(token) as c: + assert scan(c, token).to_pylist() == [{"n": 1, "text": "kept"}] diff --git a/tests/catalog/test_index_example_cross_sdk.py b/tests/catalog/test_index_example_cross_sdk.py new file mode 100644 index 00000000..ba70eac2 --- /dev/null +++ b/tests/catalog/test_index_example_cross_sdk.py @@ -0,0 +1,89 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Live typed-index discovery and refusal across independent SDK workers.""" + +from __future__ import annotations + +import json +import os + +import pytest + +from vgi.catalog import OnConflict, SchemaObjectType +from vgi.catalog.capabilities import INDEX_DEFINITION_CREATE, INDEX_DEFINITION_READ, INDEX_DROP, CatalogCapabilities +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.indexes import IndexConstraintType, IndexDefinition, IndexKey +from vgi.client import Client +from vgi.protocol import ClientCapabilities +from vgi.typed_values import TypedValueBatch + +WORKERS: dict[str, list[str]] = json.loads(os.environ.get("VGI_METADATA_WORKERS", "{}")) +OFFER = ClientCapabilities( + engine="reference-client", + native_formats=[], + catalogs=[], + can_stream=False, + filter_encodings=[], + catalog=CatalogCapabilities(read=[INDEX_DEFINITION_READ], ddl=sorted([INDEX_DEFINITION_CREATE, INDEX_DROP])), +) +UNSUPPORTED = ( + "(?i)unsupported|unimplemented|not implemented|does not support|not supported|capability|negotiated|requires" +) + + +@pytest.mark.parametrize("sdk", sorted(set(WORKERS) & {"typescript"})) +def test_live_index_discovery_has_explicit_metadata_only_definitions(sdk: str) -> None: + with Client(server_path=WORKERS[sdk], worker_limit=1) as client: + token = client.catalog_attach( + name="example", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ).attach_opaque_data + listed = client.schema_contents(attach_opaque_data=token, path=["data"], type=SchemaObjectType.INDEX) + assert {item.name for item in listed} == {"idx_numbers_value", "idx_numbers_value_unique"} + for info in listed: + fetched = client.index_get(attach_opaque_data=token, schema_path=["data"], name=info.name) + assert fetched == info + assert info.definition.table == CatalogObjectRef(kind="table", schema_path=["data"], name="numbers") + assert info.definition.algorithm == "fixture.metadata" + assert info.definition.constraint_type == IndexConstraintType.NONE + assert info.definition.associated_constraint is None + assert [key.column_path for key in info.definition.key_definitions()] == [["value"]] + assert info.options.values == info.definition.options == TypedValueBatch.empty() + assert info.options.explicit_names == [] + + +@pytest.mark.parametrize("sdk", sorted(WORKERS)) +def test_unqualified_index_mutations_leave_catalog_unchanged(sdk: str) -> None: + with Client(server_path=WORKERS[sdk], worker_limit=1) as client: + attached = client.catalog_attach( + name="contents_reval", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ) + assert attached.selected_capabilities is None or not {INDEX_DEFINITION_CREATE, INDEX_DROP}.intersection( + attached.selected_capabilities.names() + ) + token = attached.attach_opaque_data + version = client.catalog_version(attach_opaque_data=token) + before = client.contents(attach_opaque_data=token) + definition = IndexDefinition( + table=CatalogObjectRef(kind="table", schema_path=["main"], name="items"), + algorithm="fixture.metadata", + keys=[IndexKey(column_path=["id"]).serialize_to_bytes()], + options=TypedValueBatch.empty(), + constraint_type=IndexConstraintType.NONE, + ) + for conflict in (OnConflict.ERROR, OnConflict.REPLACE): + with pytest.raises(Exception, match=UNSUPPORTED): + client.index_create( + attach_opaque_data=token, + schema_path=["main"], + name="probe", + definition=definition, + on_conflict=conflict, + ) + with pytest.raises(Exception, match=UNSUPPORTED): + client.index_drop(attach_opaque_data=token, schema_path=["main"], name="probe", ignore_not_found=True) + with pytest.raises(Exception, match=UNSUPPORTED): + client.index_option_definitions( + attach_opaque_data=token, table=definition.table, algorithm=definition.algorithm + ) + assert client.catalog_version(attach_opaque_data=token) == version + assert client.contents(attach_opaque_data=token) == before diff --git a/tests/catalog/test_index_public_contract.py b/tests/catalog/test_index_public_contract.py new file mode 100644 index 00000000..fedbf009 --- /dev/null +++ b/tests/catalog/test_index_public_contract.py @@ -0,0 +1,225 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Typed index wire shape and fail-closed public capability boundaries.""" + +from __future__ import annotations + +from dataclasses import replace +from types import SimpleNamespace +from typing import Any, cast + +import pyarrow as pa +import pytest + +from vgi.catalog.capabilities import ( + CONSTRAINT_ENFORCE, + CONSTRAINT_RECORDS_READ, + INDEX_DEFINITION_CREATE, + INDEX_DEFINITION_READ, + INDEX_DROP, + ROW_SCALAR_EXPRESSION, + TRANSACTION_DDL, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import IndexInfo, OnConflict +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.indexes import IndexConstraintType, IndexDefinition, IndexKey +from vgi.catalog.options import OptionState +from vgi.catalog.sql import SqlExpression +from vgi.exceptions import CatalogCapabilityError +from vgi.meta_worker import MetaWorker +from vgi.protocol import IndexCreateRequest, VgiProtocol +from vgi.typed_values import TypedValueBatch +from vgi.worker import Worker + +TABLE = CatalogObjectRef(kind="table", schema_path=["a.b", "nested"], name="items") +DEFINITION = IndexDefinition( + table=TABLE, + algorithm="test.binary.v1", + keys=[IndexKey(column_path=["record", "a.b"]).serialize_to_bytes()], + options=TypedValueBatch.empty(), + constraint_type=IndexConstraintType.NONE, +) +PROFILES = CatalogCapabilities( + read=sorted([INDEX_DEFINITION_READ, CONSTRAINT_RECORDS_READ]), + ddl=sorted([INDEX_DEFINITION_CREATE, INDEX_DROP]), + write=[CONSTRAINT_ENFORCE], + transaction=[TRANSACTION_DDL], +) + + +def info(definition: IndexDefinition = DEFINITION) -> IndexInfo: + return IndexInfo( + name="idx", + schema_path=["indexes"], + comment=None, + tags={}, + definition=definition, + options=OptionState(values=definition.options), + ) + + +def request(**changes: Any) -> IndexCreateRequest: + return replace( + IndexCreateRequest(attach_opaque_data=b"sealed", schema_path=["indexes"], name="idx", definition=DEFINITION), + **changes, + ) + + +def worker( + monkeypatch: pytest.MonkeyPatch, selected: CatalogCapabilities = PROFILES, available: CatalogCapabilities = PROFILES +) -> tuple[Worker, SimpleNamespace, list[dict[str, Any]]]: + instance = object.__new__(Worker) + calls = [] + provider = SimpleNamespace( + catalog_capabilities=lambda **kwargs: available, + index_create=lambda **kwargs: calls.append(kwargs), + index_drop=lambda **kwargs: calls.append(kwargs), + index_get=lambda **kwargs: info(), + ) + monkeypatch.setattr(instance, "_get_catalog", lambda: provider) + monkeypatch.setattr(instance, "_catalog_agreement", lambda envelope: selected) + monkeypatch.setattr(instance, "_unwrap_attach", lambda envelope: b"provider") + monkeypatch.setattr(instance, "_unwrap_tx_opt", lambda tx, attach: None if tx is None else b"tx") + monkeypatch.setattr(instance, "_enrich_catalog_span", lambda **kwargs: None) + return instance, provider, calls + + +def test_typed_public_carriers_replace_legacy_strings_and_require_options() -> None: + assert not {"table_name", "index_type", "expressions", "constraint_type"}.intersection(IndexInfo.ARROW_SCHEMA.names) + assert not {"table_name", "index_type", "expressions", "options", "constraint_type"}.intersection( + IndexCreateRequest.ARROW_SCHEMA.names + ) + cases: list[tuple[type[IndexDefinition] | type[IndexInfo] | type[IndexCreateRequest], list[str]]] = [ + (IndexDefinition, ["table", "algorithm", "keys", "options", "constraint_type"]), + (IndexInfo, ["definition", "options"]), + (IndexCreateRequest, ["definition"]), + ] + for cls, names in cases: + assert all(not cls.ARROW_SCHEMA.field(name).nullable for name in names) + assert IndexInfo.deserialize_from_bytes(info().serialize_to_bytes()) == info() + assert IndexCreateRequest.deserialize_from_bytes(request().serialize_to_bytes()) == request() + assert DEFINITION.key_definitions()[0].column_path == ["record", "a.b"] + + +@pytest.mark.parametrize("kwargs", [{}, {"column_path": []}, {"column_path": ["x", None]}, {"column_path": [""]}]) +def test_invalid_keys_reject(kwargs: dict[str, Any]) -> None: + with pytest.raises((ValueError, TypeError)): + IndexKey(**kwargs) + + +def test_missing_options_empty_algorithm_and_key_list_reject() -> None: + for changes in [{"options": None}, {"algorithm": ""}, {"keys": []}, {"keys": [None]}, {"keys": [b"bad"]}]: + with pytest.raises((ValueError, TypeError, pa.ArrowInvalid)): + replace(DEFINITION, **cast(dict[str, Any], changes)) + + +def test_primary_requires_actual_owned_constraint_identity() -> None: + with pytest.raises(ValueError, match="association"): + replace(DEFINITION, constraint_type=IndexConstraintType.PRIMARY) + association = CatalogObjectRef(kind="constraint", schema_path=TABLE.schema_path, owner_table="other", name="stable") + with pytest.raises(ValueError, match="another table"): + replace(DEFINITION, constraint_type=IndexConstraintType.UNIQUE, associated_constraint=association) + + +def test_strict_record_rejects_trailing_data_missing_fields_and_mutable_damage() -> None: + with pytest.raises(ValueError): + IndexDefinition.deserialize_from_bytes(DEFINITION.serialize_to_bytes() + b"trailing") + batch = TypedValueBatch.deserialize_from_bytes(DEFINITION.serialize_to_bytes()).batch + with pytest.raises(ValueError): + IndexDefinition.deserialize_from_batch(batch.drop_columns(["keys"])) + broken = replace(DEFINITION, keys=list(DEFINITION.keys)) + broken.keys.append(b"") + with pytest.raises(ValueError): + broken.serialize_to_bytes() + + +def test_readback_option_values_must_match_resolved_definition() -> None: + other = TypedValueBatch(pa.record_batch([pa.array([1], type=pa.int64())], names=["limit"])) + with pytest.raises(ValueError, match="disagree"): + replace(info(), options=OptionState(values=other, explicit_names=["limit"])) + + +def test_profiles_require_readback_and_do_not_select_without_offer() -> None: + assert PROFILES.select(None) == CatalogCapabilities() + assert PROFILES.select(CatalogCapabilities(ddl=[INDEX_DEFINITION_CREATE, INDEX_DROP])).ddl == [] + + +@pytest.mark.parametrize("selected", [CatalogCapabilities(), CatalogCapabilities(read=[INDEX_DEFINITION_READ])]) +def test_create_and_drop_without_selected_profiles_have_no_effect( + monkeypatch: pytest.MonkeyPatch, selected: CatalogCapabilities +) -> None: + instance, _, calls = worker(monkeypatch, selected=selected) + with pytest.raises(CatalogCapabilityError): + instance.catalog_index_create(request()) + with pytest.raises(CatalogCapabilityError): + instance.catalog_index_drop(b"sealed", ["indexes"], "idx") + assert calls == [] + + +def test_current_provider_profile_revocation_and_transactions_have_no_effect(monkeypatch: pytest.MonkeyPatch) -> None: + instance, _, calls = worker(monkeypatch, available=CatalogCapabilities()) + with pytest.raises(CatalogCapabilityError): + instance.catalog_index_create(request()) + instance, _, calls = worker(monkeypatch, selected=replace(PROFILES, transaction=[])) + with pytest.raises(CatalogCapabilityError): + instance.catalog_index_create(request(transaction_opaque_data=b"tx")) + assert calls == [] + + +def test_unique_requires_enforcement_and_replace_requires_drop(monkeypatch: pytest.MonkeyPatch) -> None: + instance, _, calls = worker(monkeypatch, selected=replace(PROFILES, write=[])) + with pytest.raises(CatalogCapabilityError): + instance.catalog_index_create( + request(definition=replace(DEFINITION, constraint_type=IndexConstraintType.UNIQUE)) + ) + instance, _, calls = worker(monkeypatch, selected=replace(PROFILES, ddl=[INDEX_DEFINITION_CREATE])) + with pytest.raises(CatalogCapabilityError): + instance.catalog_index_create(request(on_conflict=OnConflict.REPLACE)) + assert calls == [] + + +def test_definition_is_forwarded_without_raw_name_or_option_changes(monkeypatch: pytest.MonkeyPatch) -> None: + instance, _, calls = worker(monkeypatch) + instance.catalog_index_create(request()) + assert calls[0]["definition"] == DEFINITION + assert calls[0]["attach_opaque_data"] == b"provider" + assert calls[0]["schema_path"] == ["indexes"] + assert calls[0]["definition"].table.schema_path == ["a.b", "nested"] + + +def test_index_discovery_never_silently_drops_required_definition() -> None: + with pytest.raises(CatalogCapabilityError): + Worker._index_for_capabilities(info(), CatalogCapabilities()) + assert Worker._index_for_capabilities(info(), PROFILES) == info() + + +def test_meta_worker_routes_new_index_options(monkeypatch: pytest.MonkeyPatch) -> None: + meta = object.__new__(MetaWorker) + calls = [] + child = SimpleNamespace(catalog_index_option_definitions=lambda **kwargs: calls.append(kwargs)) + monkeypatch.setattr(meta, "_worker_for_attach", lambda *args, **kwargs: child) + cast(VgiProtocol, meta).catalog_index_option_definitions( + attach_opaque_data=b"sealed", table=TABLE.serialize_to_bytes(), algorithm="test.binary.v1" + ) + assert calls[0]["table"] == TABLE.serialize_to_bytes() + + +def test_expression_requires_context_matching_table_and_selected_profile(monkeypatch: pytest.MonkeyPatch) -> None: + expression = SqlExpression( + sql='lower("label")', + profile=ROW_SCALAR_EXPRESSION, + schema_path=TABLE.schema_path, + evaluation_context=QueryEvaluationContext(), + ) + with pytest.raises(ValueError, match="exactly one"): + IndexKey(column_path=["label"], expression=expression) + value = replace(DEFINITION, keys=[IndexKey(expression=expression).serialize_to_bytes()]) + instance, _, calls = worker(monkeypatch) + with pytest.raises(CatalogCapabilityError): + instance.catalog_index_create(request(definition=value)) + assert calls == [] + with pytest.raises(ValueError, match="table schema"): + replace(value, table=replace(TABLE, schema_path=["different"])) diff --git a/tests/catalog/test_index_public_lifecycle.py b/tests/catalog/test_index_public_lifecycle.py new file mode 100644 index 00000000..dad5fd33 --- /dev/null +++ b/tests/catalog/test_index_public_lifecycle.py @@ -0,0 +1,231 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Public HTTP lifecycle proving real SQLite indexes and atomic catalog changes.""" + +from __future__ import annotations + +import sqlite3 +from dataclasses import replace +from pathlib import Path +from typing import Any + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.catalog import IndexInfo, OnConflict, SchemaObjectType +from vgi.catalog.catalog_interface import AttachOpaqueData, TransactionOpaqueData +from vgi.catalog.indexes import IndexConstraintType, IndexDefinition, IndexKey +from vgi.client import Client +from vgi.client.catalog_mixin import CatalogClientError +from vgi.protocol import ClientCapabilities +from vgi.serve import create_app +from vgi.typed_values import TypedValueBatch + +from .index_protocol_reference import ALGORITHM, PROFILES, TABLE, IndexProvider, IndexWorker +from .typing_helpers import present + +OFFER = ClientCapabilities( + engine="reference", native_formats=[], catalogs=[], can_stream=False, filter_encodings=[], catalog=PROFILES +) + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> IndexProvider: + monkeypatch.setattr(IndexProvider, "database_path", str(tmp_path / "indexes.sqlite"), raising=False) + return IndexProvider() + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", + httpx_client=_SyncTestClient( + create_app(IndexWorker, describe=False, signing_key=b"index-reference-signing-key") + ), + ) + + +def attach(c: Client) -> AttachOpaqueData: + return c.catalog_attach( + name="indexes", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ).attach_opaque_data + + +def definition(*columns: str, unique: bool = False) -> IndexDefinition: + return IndexDefinition( + table=TABLE, + algorithm=ALGORITHM, + keys=[IndexKey(column_path=[column]).serialize_to_bytes() for column in columns or ("id",)], + options=TypedValueBatch.empty(), + constraint_type=IndexConstraintType.UNIQUE if unique else IndexConstraintType.NONE, + ) + + +def create( + c: Client, + token: AttachOpaqueData, + name: str = "idx", + value: IndexDefinition | None = None, + tx: TransactionOpaqueData | None = None, + conflict: OnConflict = OnConflict.ERROR, +) -> None: + c.index_create( + attach_opaque_data=token, + schema_path=["main"], + name=name, + definition=definition() if value is None else value, + transaction_opaque_data=tx, + on_conflict=conflict, + ) + + +def get( + c: Client, token: AttachOpaqueData, name: str = "idx", tx: TransactionOpaqueData | None = None +) -> IndexInfo | None: + return c.index_get(attach_opaque_data=token, schema_path=["main"], name=name, transaction_opaque_data=tx) + + +def drop( + c: Client, token: AttachOpaqueData, name: str = "idx", tx: TransactionOpaqueData | None = None, **kwargs: Any +) -> None: + c.index_drop(attach_opaque_data=token, schema_path=["main"], name=name, transaction_opaque_data=tx, **kwargs) + + +def test_real_index_discovery_options_reconnect_and_drop(backend: IndexProvider) -> None: + with client() as c: + token = attach(c) + options = c.index_option_definitions(attach_opaque_data=token, table=TABLE, algorithm=ALGORITHM) + assert options.schema.names == [] and options.create_names == [] + create(c, token, value=definition("id", "label")) + original = present(get(c, token)) + assert original.definition == definition("id", "label") + assert original.options.explicit_names == [] + assert c.contents(attach_opaque_data=token).schemas[0].indexes == [original] + assert list(c.schema_contents(attach_opaque_data=token, path=["main"], type=SchemaObjectType.INDEX)) == [ + original + ] + # Fresh worker instance resolves durable metadata and verifies physical index. + with client() as c: + assert get(c, token) == original + drop(c, token) + assert get(c, token) is None + drop(c, token, ignore_not_found=True) + + +def test_unique_is_enforced_on_real_rows_without_inventing_logical_primary(backend: IndexProvider) -> None: + provider_token = backend.authority.attach() + with client() as c: + token = attach(c) + create(c, token, value=definition(unique=True)) + assert present(get(c, token)).definition.associated_constraint is None + table = c.table_get(attach_opaque_data=token, schema_path=["main"], name="items") + assert present(table).constraints == [] + before = backend.authority.rows(provider_token) + with pytest.raises(sqlite3.IntegrityError): + backend.authority.replace_rows(provider_token, [{"id": 1, "label": "a"}, {"id": 1, "label": "b"}]) + assert backend.authority.rows(provider_token) == before + drop(c, token) + backend.authority.replace_rows(provider_token, [{"id": 1, "label": "a"}, {"id": 1, "label": "b"}]) + with pytest.raises(CatalogClientError, match="UNIQUE"): + create(c, token, value=definition(unique=True)) + assert get(c, token) is None + + +def test_replacement_failure_preserves_original_physical_index_and_metadata(backend: IndexProvider) -> None: + provider_token = backend.authority.attach() + backend.authority.replace_rows(provider_token, [{"id": 1, "label": "a"}, {"id": 1, "label": "b"}]) + with client() as c: + token = attach(c) + create(c, token) + original = present(get(c, token)) + with pytest.raises(CatalogClientError, match="UNIQUE"): + create(c, token, value=definition(unique=True), conflict=OnConflict.REPLACE) + assert get(c, token) == original + create(c, token, value=definition("label"), conflict=OnConflict.REPLACE) + assert present(get(c, token)).definition == definition("label") + + +@pytest.mark.parametrize( + "value", + [ + replace(definition(), algorithm="ART"), + replace(definition(), keys=[IndexKey(column_path=["id", "child"]).serialize_to_bytes()]), + replace(definition(), options=TypedValueBatch(pa.record_batch([pa.array([1])], names=["limit"]))), + ], +) +def test_unsupported_replacement_rejects_before_effects(backend: IndexProvider, value: IndexDefinition) -> None: + with client() as c: + token = attach(c) + create(c, token) + original = present(get(c, token)) + with pytest.raises(CatalogClientError): + create(c, token, value=value, conflict=OnConflict.REPLACE) + assert get(c, token) == original + with pytest.raises(CatalogClientError, match="CASCADE"): + drop(c, token, cascade=True) + assert get(c, token) == original + + +def test_transaction_local_physical_indexes_rollback_commit_and_attachment_scope(backend: IndexProvider) -> None: + with client() as c: + token, other = attach(c), attach(c) + tx = c.catalog_transaction_begin(attach_opaque_data=token) + create(c, token, tx=tx) + assert get(c, token, tx=tx) is not None and get(c, token) is None + with pytest.raises(CatalogClientError, match="transaction"): + get(c, other, tx=tx) + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert get(c, token) is None + tx = c.catalog_transaction_begin(attach_opaque_data=token) + create(c, token, tx=tx) + with client() as c: + assert get(c, token, tx=tx) is not None + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert get(c, token) is not None + with pytest.raises(CatalogClientError, match="transaction"): + get(c, token, tx=tx) + tx = c.catalog_transaction_begin(attach_opaque_data=token) + drop(c, token, tx=tx) + assert get(c, token, tx=tx) is None and get(c, token) is not None + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert get(c, token) is not None + + +def test_stale_transaction_cannot_overwrite_new_committed_index(backend: IndexProvider) -> None: + with client() as c: + token = attach(c) + tx = c.catalog_transaction_begin(attach_opaque_data=token) + create(c, token, "pending", tx=tx) + create(c, token, "committed") + with pytest.raises(CatalogClientError, match="revision"): + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert get(c, token, "committed") is not None and get(c, token, "pending") is None + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + + +def test_schema_population_tracks_committed_and_transaction_local_indexes(backend: IndexProvider) -> None: + with client() as c: + token = attach(c) + + def count(tx: TransactionOpaqueData | None = None) -> int: + schemas = c.schemas(attach_opaque_data=token, transaction_opaque_data=tx) + direct = c.schema_get(attach_opaque_data=token, transaction_opaque_data=tx, path=["main"]) + assert schemas == [direct] + assert present(present(direct).estimated_object_count)["table"] == 1 + assert present(present(direct).estimated_object_count)["table_function"] == 1 + return present(present(direct).estimated_object_count)["index"] + + assert count() == 0 + create(c, token) + assert count() == 1 + assert present(c.contents(attach_opaque_data=token).schemas[0].schema.estimated_object_count)["index"] == 1 + tx = c.catalog_transaction_begin(attach_opaque_data=token) + create(c, token, "pending", tx=tx) + assert count(tx) == 2 and count() == 1 + drop(c, token, tx=tx) + assert count(tx) == 1 and count() == 1 + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert count() == 1 + drop(c, token) + assert count() == 0 diff --git a/tests/catalog/test_integration.py b/tests/catalog/test_integration.py index 69c08a54..9198b3e1 100644 --- a/tests/catalog/test_integration.py +++ b/tests/catalog/test_integration.py @@ -9,6 +9,8 @@ to ensure each test gets a fresh worker process with a fresh InMemoryCatalog. """ +from __future__ import annotations + import sys from collections.abc import Iterator @@ -18,10 +20,15 @@ from vgi import schema from vgi.catalog import MacroType, OnConflict, SchemaObjectType, SerializedSchema +from vgi.catalog.constraints import ConstraintDefinition, ConstraintKind +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.macros import legacy_macro_fields, normalize_macro_overloads from vgi.client import Client from vgi.client import catalog_mixin as _cm from vgi.client.catalog_mixin import CatalogClientError +from .typing_helpers import CatalogObjectArgs, CatalogPathArgs, present + CATALOG_WORKER = f"{sys.executable} -m vgi._test_fixtures.catalog" @@ -166,7 +173,7 @@ class TestCatalogTables: """Test table operations via client→worker round-trip.""" def test_table_create_and_get(self) -> None: - """Can create and retrieve a table with constraints.""" + """Metadata-only catalogs return the explicit empty canonical list.""" client = Client(CATALOG_WORKER) result = client.catalog_attach(name="memory", options={}, data_version_spec=None, implementation_version=None) @@ -179,9 +186,7 @@ def test_table_create_and_get(self) -> None: name="users", columns=columns, on_conflict=OnConflict.ERROR, - not_null_constraints=[0], - unique_constraints=[[0]], - check_constraints=[], + constraint_definitions=[], ) table = client.table_get( @@ -194,8 +199,33 @@ def test_table_create_and_get(self) -> None: assert table.name == "users" assert table.schema_path == ["main"] assert table.columns == columns - assert table.not_null_constraints == [0] - assert table.unique_constraints == [[0]] + assert table.constraints == [] + assert table.definition_revision is None + + def test_metadata_only_catalog_rejects_constraint_creation_and_mutation(self) -> None: + """An unqualified catalog cannot claim or silently ignore enforcement.""" + client = Client(CATALOG_WORKER) + result = client.catalog_attach(name="memory", options={}, data_version_spec=None, implementation_version=None) + args: CatalogObjectArgs = dict( + attach_opaque_data=result.attach_opaque_data, schema_path=["main"], name="constrained" + ) + version = client.catalog_version(attach_opaque_data=result.attach_opaque_data) + columns = pa.schema([pa.field("id", pa.int64(), nullable=False)]) + with pytest.raises(CatalogClientError, match="vgi.constraint.create.v1"): + client.table_create( + **args, + columns=SerializedSchema(columns.serialize().to_pybytes()), + constraint_definitions=[ + ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[["id"]]).serialize_to_bytes() + ], + ) + assert client.table_get(**args) is None + assert client.catalog_version(attach_opaque_data=result.attach_opaque_data) == version + client.table_create(**args, columns=SerializedSchema(schema(id=pa.int64()).serialize().to_pybytes())) + for operation in (client.table_not_null_set, client.table_not_null_drop): + with pytest.raises(CatalogClientError, match="vgi.constraint.mutate.v1"): + operation(**args, column_path=["id"]) + assert present(client.table_get(**args)).constraints == [] def test_table_get_nonexistent(self) -> None: """Getting nonexistent table returns None.""" @@ -312,6 +342,7 @@ def test_view_create_and_get(self) -> None: schema_path=["main"], name="user_view", definition="SELECT * FROM users", + aliases=["a.b", 'with"quote'], on_conflict=OnConflict.ERROR, ) @@ -322,9 +353,21 @@ def test_view_create_and_get(self) -> None: ) assert view is not None + assert view.aliases == ["a.b", 'with"quote'] assert view.name == "user_view" assert view.definition == "SELECT * FROM users" + def test_aliases_survive_rename_and_comment_update(self) -> None: + """Metadata-only mutations retain output identifiers.""" + client = Client(CATALOG_WORKER) + attached = client.catalog_attach(name="memory", options={}, data_version_spec=None, implementation_version=None) + target: CatalogPathArgs = dict(attach_opaque_data=attached.attach_opaque_data, schema_path=["main"]) + client.view_create(**target, name="old", definition="SELECT 1 AS x", aliases=["a.b"]) + client.view_rename(**target, name="old", new_name="renamed") + client.view_comment_set(**target, name="renamed", comment="kept") + info = client.view_get(**target, name="renamed") + assert info is not None and info.aliases == ["a.b"] and info.comment == "kept" + def test_view_drop(self) -> None: """Can create and drop a view.""" client = Client(CATALOG_WORKER) @@ -561,8 +604,15 @@ def test_macro_create_and_get(self) -> None: schema_path=["main"], name="double", macro_type=MacroType.SCALAR, - parameters=["x"], - definition="x * 2", + overloads=normalize_macro_overloads( + macro_type=(MacroType.SCALAR).value, + schema_path=["main"], + parameters=["x"], + definition="x * 2", + parameter_default_values=None, + arguments_schema=None, + evaluation_context=QueryEvaluationContext(), + ), ) macro = client.macro_get( @@ -575,8 +625,8 @@ def test_macro_create_and_get(self) -> None: assert macro.name == "double" assert macro.schema_path == ["main"] assert macro.macro_type == MacroType.SCALAR - assert macro.parameters == ["x"] - assert macro.definition == "x * 2" + assert [p.name for p in macro.overloads[0].parameters] == ["x"] + assert macro.overloads[0].definition == "x * 2" def test_macro_drop(self) -> None: """Can create and drop a macro.""" @@ -588,8 +638,15 @@ def test_macro_drop(self) -> None: schema_path=["main"], name="to_drop", macro_type=MacroType.SCALAR, - parameters=["x"], - definition="x", + overloads=normalize_macro_overloads( + macro_type=(MacroType.SCALAR).value, + schema_path=["main"], + parameters=["x"], + definition="x", + parameter_default_values=None, + arguments_schema=None, + evaluation_context=QueryEvaluationContext(), + ), ) client.macro_drop( @@ -640,16 +697,30 @@ def test_schema_contents_filters_by_type(self) -> None: schema_path=["main"], name="scalar_one", macro_type=MacroType.SCALAR, - parameters=["x"], - definition="x", + overloads=normalize_macro_overloads( + macro_type=(MacroType.SCALAR).value, + schema_path=["main"], + parameters=["x"], + definition="x", + parameter_default_values=None, + arguments_schema=None, + evaluation_context=QueryEvaluationContext(), + ), ) client.macro_create( attach_opaque_data=result.attach_opaque_data, schema_path=["main"], name="table_one", macro_type=MacroType.TABLE, - parameters=["n"], - definition="SELECT * FROM range(n)", + overloads=normalize_macro_overloads( + macro_type=(MacroType.TABLE).value, + schema_path=["main"], + parameters=["n"], + definition="SELECT * FROM range(n)", + parameter_default_values=None, + arguments_schema=None, + evaluation_context=QueryEvaluationContext(), + ), ) scalar_macros = client.schema_contents( @@ -682,9 +753,15 @@ def test_parameter_default_values_preserved(self) -> None: schema_path=["main"], name="clamp", macro_type=MacroType.SCALAR, - parameters=["val", "lo", "hi"], - definition="GREATEST(lo, LEAST(hi, val))", - parameter_default_values=defaults, + overloads=normalize_macro_overloads( + macro_type=(MacroType.SCALAR).value, + schema_path=["main"], + parameters=["val", "lo", "hi"], + definition="GREATEST(lo, LEAST(hi, val))", + parameter_default_values=defaults, + arguments_schema=None, + evaluation_context=QueryEvaluationContext(), + ), ) macro = client.macro_get( @@ -694,8 +771,15 @@ def test_parameter_default_values_preserved(self) -> None: ) assert macro is not None - assert macro.parameter_default_values is not None - restored = macro.parameter_default_values + assert ( + legacy_macro_fields(macro.overloads, evaluation_context=QueryEvaluationContext())[ + "parameter_default_values" + ] + is not None + ) + restored = legacy_macro_fields(macro.overloads, evaluation_context=QueryEvaluationContext())[ + "parameter_default_values" + ] assert restored.num_rows == 1 assert set(restored.schema.names) == {"lo", "hi"} assert restored.column("lo").type == pa.int64() diff --git a/tests/catalog/test_logical_type_gates.py b/tests/catalog/test_logical_type_gates.py new file mode 100644 index 00000000..49d06744 --- /dev/null +++ b/tests/catalog/test_logical_type_gates.py @@ -0,0 +1,217 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Logical annotations must be negotiated before catalog effects or query binding.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path +from unittest.mock import Mock + +import pyarrow as pa +import pytest + +from vgi.catalog import CatalogAttachResult +from vgi.catalog.capabilities import MACRO_OVERLOADS_READ, CatalogCapabilities +from vgi.catalog.catalog_interface import MacroInfo, MacroType, OnConflict, SerializedSchema, TableInfo +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.macros import MACRO_PROFILE, MacroOverload, MacroParameter +from vgi.catalog.query import CatalogQueryPrepareRequest, CatalogQueryPrepareResult +from vgi.logical_types import tuple_field, variant_field +from vgi.protocol import CatalogAttachRequest, MacroCreateRequest, TableCreateRequest +from vgi.query_parameters import QueryParameters +from vgi.query_provider import QueryOwner +from vgi.typed_values import TypedValueBatch +from vgi.worker import Worker + +from .query_reference import QueryReferenceCatalog, QueryReferenceProvider, QueryReferenceWorker +from .test_query_protocol import CAPS, KEY, request + + +def schema(name: str = "x") -> pa.Schema: + return pa.schema([tuple_field(name, [pa.field("element1", pa.int64())])]) + + +def values() -> TypedValueBatch: + definition = variant_field("x") + # NULL still has a declared logical type and cannot bypass negotiation. + return TypedValueBatch( + pa.RecordBatch.from_arrays([pa.array([None], type=definition.type)], schema=pa.schema([definition])) + ) + + +def macro(parameter: MacroParameter | None = None, *, legacy: bool = False) -> MacroInfo: + if legacy: + parameter = MacroParameter(name="x", default_value=values()) + assert parameter is not None + return MacroInfo( + comment=None, + tags={}, + name="m", + schema_path=["main"], + macro_type=MacroType.SCALAR, + overloads=[ + MacroOverload( + evaluation_context=QueryEvaluationContext(), + parameters=[parameter], + definition="x", + profile=MACRO_PROFILE, + schema_path=["main"], + macro_type="scalar", + ) + ], + ) + + +def worker_without_logical_profiles() -> Mock: + worker = Mock(spec=Worker) + worker._effective_catalog_capabilities.return_value = CatalogCapabilities(read=[MACRO_OVERLOADS_READ]) + return worker + + +def test_table_discovery_rejects_unnegotiated_logical_schema() -> None: + info = TableInfo( + comment=None, + tags={}, + name="t", + schema_path=["main"], + columns=SerializedSchema(schema().serialize().to_pybytes()), + constraints=[], + ) + with pytest.raises(ValueError, match="not negotiated"): + Worker._table_for_capabilities(info, CatalogCapabilities()) + + +def test_table_creation_rejects_unnegotiated_logical_schema_before_provider() -> None: + worker = worker_without_logical_profiles() + create = TableCreateRequest( + constraint_definitions=[], + attach_opaque_data=b"attach", + schema_path=["main"], + name="t", + columns=schema().serialize().to_pybytes(), + on_conflict=OnConflict.ERROR, + ) + with pytest.raises(ValueError, match="not negotiated"): + Worker.catalog_table_create(worker, create) + worker._get_catalog.return_value.table_create.assert_not_called() + + +@pytest.mark.parametrize("kind", ["declared", "default", "legacy_default"]) +def test_macro_discovery_rejects_unnegotiated_logical_parameters(kind: str) -> None: + parameter = ( + MacroParameter(name="x", declared_type=schema()) + if kind == "declared" + else MacroParameter(name="x", default_value=values()) + ) + info = macro(parameter, legacy=kind == "legacy_default") + with pytest.raises(ValueError, match="not negotiated"): + Worker._macro_for_capabilities(info, CatalogCapabilities(read=[MACRO_OVERLOADS_READ])) + + +@pytest.mark.parametrize("kind", ["declared", "default", "legacy_default"]) +def test_macro_creation_rejects_unnegotiated_logical_parameters_before_provider(kind: str) -> None: + parameter = ( + MacroParameter(name="x", declared_type=schema()) + if kind == "declared" + else MacroParameter(name="x", default_value=values()) + ) + info = macro(parameter, legacy=kind == "legacy_default") + create = MacroCreateRequest( + attach_opaque_data=b"attach", + schema_path=info.schema_path, + name=info.name, + macro_type=info.macro_type, + overloads=info.overloads, + on_conflict=OnConflict.ERROR, + ) + worker = worker_without_logical_profiles() + with pytest.raises(ValueError, match="not negotiated"): + Worker.catalog_macro_create(worker, create) + worker._get_catalog.return_value.macro_create.assert_not_called() + + +@pytest.fixture +def query_backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[QueryReferenceWorker, CatalogAttachResult]: + monkeypatch.setattr(QueryReferenceCatalog, "database_path", str(tmp_path / "queries.sqlite"), raising=False) + worker = QueryReferenceWorker(quiet=True) + worker._signing_key = KEY + attached = worker.catalog_attach( + CatalogAttachRequest( + name="queries", + data_version_spec=None, + implementation_version=None, + client_capabilities=CAPS, + ) + ) + return worker, attached + + +def test_query_preparation_rejects_unnegotiated_parameter_schema_before_provider( + query_backend: tuple[QueryReferenceWorker, CatalogAttachResult], monkeypatch: pytest.MonkeyPatch +) -> None: + worker, attached = query_backend + provider = worker._get_catalog().query_provider + assert isinstance(provider, QueryReferenceProvider) + prepare = Mock(wraps=provider.prepare) + monkeypatch.setattr(provider, "prepare", prepare) + field = variant_field("value") + parameters = QueryParameters.from_batch( + pa.RecordBatch.from_arrays([pa.array([None], type=field.type)], schema=pa.schema([field])), + mode="named", + ) + with pytest.raises(ValueError, match="not negotiated"): + worker.catalog_query_prepare(request(attached, parameters)) + prepare.assert_not_called() + + +def test_query_preparation_rejects_unnegotiated_output_and_releases_recipe( + query_backend: tuple[QueryReferenceWorker, CatalogAttachResult], monkeypatch: pytest.MonkeyPatch +) -> None: + worker, attached = query_backend + provider = worker._get_catalog().query_provider + assert isinstance(provider, QueryReferenceProvider) + original = provider.prepare + + def unexpected_output(*, owner: QueryOwner, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + return replace(original(owner=owner, request=request), output_schema=schema("result").serialize().to_pybytes()) + + release = Mock(wraps=provider.release) + monkeypatch.setattr(provider, "prepare", unexpected_output) + monkeypatch.setattr(provider, "release", release) + with pytest.raises(ValueError, match="not negotiated"): + worker.catalog_query_prepare(request(attached)) + release.assert_called_once() + + +@pytest.mark.parametrize("operation", ["replace", "drop"]) +def test_mandatory_family_mutations_do_not_require_representation_capability( + query_backend: tuple[QueryReferenceWorker, CatalogAttachResult], monkeypatch: pytest.MonkeyPatch, operation: str +) -> None: + worker, attached = query_backend + catalog = worker._get_catalog() + info = macro(MacroParameter(name="x")) + monkeypatch.setattr(catalog, "macro_get", lambda **kwargs: info) + mutation = Mock() + monkeypatch.setattr(catalog, "macro_create", mutation) + monkeypatch.setattr(catalog, "macro_drop", mutation) + if operation == "replace": + worker.catalog_macro_create( + MacroCreateRequest( + attach_opaque_data=attached.attach_opaque_data, + schema_path=["main"], + name="m", + macro_type=MacroType.SCALAR, + overloads=info.overloads, + on_conflict=OnConflict.REPLACE, + ) + ) + else: + worker.catalog_macro_drop( + attach_opaque_data=attached.attach_opaque_data, + schema_path=["main"], + name="m", + ignore_not_found=False, + transaction_opaque_data=None, + ) + mutation.assert_called_once() diff --git a/tests/catalog/test_macro_contract.py b/tests/catalog/test_macro_contract.py new file mode 100644 index 00000000..12becf64 --- /dev/null +++ b/tests/catalog/test_macro_contract.py @@ -0,0 +1,375 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Macro declaration fidelity and atomic create/discovery reference behavior.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import Any, NotRequired, TypedDict + +import duckdb +import pyarrow as pa +import pytest + +from vgi.argument_spec import macro_arguments_schema +from vgi.catalog import MacroType +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.layout import SqlExpression +from vgi.catalog.macros import ( + MACRO_DEFAULT_PROFILE, + MACRO_PROFILE, + MacroOverload, + MacroParameter, + canonicalize_macro_defaults, + legacy_macro_fields, + normalize_macro_overloads, + validate_macro_overloads, +) +from vgi.typed_values import TypedValueBatch + +from .typing_helpers import present + + +def literal( + name: str = "x", + value: object = None, + dtype: pa.DataType | None = None, + *, + metadata: dict[bytes | str, bytes | str] | None = None, +) -> TypedValueBatch: + dtype = dtype if dtype is not None else pa.int64() + schema = pa.schema([pa.field(name, dtype, metadata={b"logical": b"keep"})], metadata=metadata) + return TypedValueBatch(pa.RecordBatch.from_arrays([pa.array([value], type=dtype)], schema=schema)) + + +def overload(parameters: list[MacroParameter] | None = None, **changes: Any) -> MacroOverload: + return MacroOverload( + evaluation_context=changes.pop("evaluation_context", QueryEvaluationContext()), + parameters=parameters if parameters is not None else [MacroParameter(name="x")], + definition=changes.pop("definition", "x + 1"), + profile=MACRO_PROFILE, + schema_path=changes.pop("schema_path", ["a.b", "nested"]), + macro_type=changes.pop("macro_type", "scalar"), + **changes, + ) + + +def fields(**changes: Any) -> dict[str, Any]: + return dict(macro_type="scalar", schema_path=["a.b", "nested"], parameters=[], definition="", **changes) + + +def test_parameter_preserves_declared_type_and_recursive_metadata_without_inference() -> None: + schema = pa.schema( + [pa.field("x", pa.struct([pa.field("a", pa.int64(), metadata={b"nested": b"identity"})]))], + metadata={b"schema": b"identity"}, + ) + parameter = MacroParameter(name="x", declared_type=schema, default_value=literal(value=None)) + restored = MacroParameter.deserialize_from_bytes(parameter.serialize_to_bytes()) + assert present(restored.declared_type).equals(schema, check_metadata=True) + assert present(restored.default_value).schema.equals(present(parameter.default_value).schema, check_metadata=True) + assert restored.has_default + assert present(restored.default_value).batch.column(0)[0].as_py() is None + assert MacroParameter(name="x", default_value=literal(value=7)).declared_type is None + assert not MacroParameter(name="x").has_default + + +def test_expression_default_and_body_keep_raw_nested_owner_and_order() -> None: + expression = SqlExpression( + evaluation_context=QueryEvaluationContext(), + sql="40 + 2", + profile=MACRO_DEFAULT_PROFILE, + schema_path=["a.b", "nested"], + ) + original = overload([MacroParameter(name="required"), MacroParameter(name="x", default_expression=expression)]) + copied = MacroOverload.deserialize_from_bytes(original.serialize_to_bytes()) + assert [parameter.name for parameter in copied.parameters] == ["required", "x"] + assert copied.parameters[1].default_expression == expression + assert copied.schema_path == ["a.b", "nested"] + assert copied.parameters[1].default_value is None + + +@pytest.mark.parametrize( + "changes", + [ + {"name": ""}, + {"name": "a\0b"}, + {"declared_type": pa.schema([])}, + {"declared_type": pa.schema([pa.field("other", pa.int32())])}, + {"declared_type": pa.schema([pa.field("x", pa.int32(), nullable=False)])}, + {"declared_type": pa.schema([pa.field("x", pa.null())])}, + {"default_value": literal(name="other")}, + {"documentation": "bad\0doc"}, + ], +) +def test_invalid_parameter_contracts_reject(changes: dict[str, Any]) -> None: + with pytest.raises((ValueError, TypeError)): + replace(MacroParameter(name="x"), **changes) + + +def test_default_value_and_expression_cannot_coexist() -> None: + expression = SqlExpression( + evaluation_context=QueryEvaluationContext(), sql="NULL", profile=MACRO_DEFAULT_PROFILE, schema_path=["main"] + ) + with pytest.raises(ValueError, match="mutually exclusive"): + MacroParameter(name="x", default_value=literal(), default_expression=expression) + + +def test_unknown_default_profile_rejects_before_provider_evaluation() -> None: + with pytest.raises(ValueError, match="unsupported"): + MacroParameter( + name="x", + default_expression=SqlExpression( + evaluation_context=QueryEvaluationContext(), + sql="2 + 2", + profile="unknown.default.v1", + schema_path=["main"], + ), + ) + + +@pytest.mark.parametrize( + "changes", + [ + {"macro_type": "aggregate"}, + {"profile": "unknown.macro.v1"}, + {"definition": " "}, + {"definition": "x\0"}, + {"schema_path": []}, + {"schema_path": ["bad\0schema"]}, + {"parameters": [MacroParameter(name="x"), MacroParameter(name="X")]}, + {"parameters": [MacroParameter(name="x", default_value=literal()), MacroParameter(name="y")]}, + ], +) +def test_invalid_overload_contracts_reject(changes: dict[str, Any]) -> None: + with pytest.raises((ValueError, TypeError)): + replace(overload(), **changes) + + +def test_default_expression_cannot_rebind_to_another_owner() -> None: + with pytest.raises(ValueError, match="owner schema"): + overload( + [ + MacroParameter( + name="x", + default_expression=SqlExpression( + evaluation_context=QueryEvaluationContext(), + sql="1", + profile=MACRO_DEFAULT_PROFILE, + schema_path=["main"], + ), + ) + ] + ) + + +@pytest.mark.parametrize( + "changes", + [ + {"parameters": ["x"]}, + {"definition": "x + 1"}, + {"parameter_default_values": literal().batch}, + {"arguments_schema": pa.schema([])}, + ], +) +def test_dual_legacy_and_new_forms_reject_even_if_first_overload_matches(changes: dict[str, Any]) -> None: + args = fields(overloads=[overload()]) + args.update(changes) + with pytest.raises(ValueError, match="conflicts"): + validate_macro_overloads(**args) + + +def test_collection_is_complete_nonempty_homogeneous_and_owner_relative() -> None: + for collection in [[], [overload(macro_type="table")], [overload(schema_path=["other"])]]: + with pytest.raises(ValueError): + validate_macro_overloads(**fields(overloads=collection)) + first = overload() + second = overload([MacroParameter(name="x"), MacroParameter(name="y")], definition="x + y") + copied = normalize_macro_overloads(**fields(overloads=[second, first])) + assert [len(item.parameters) for item in copied] == [2, 1] + first.parameters.clear() + assert len(copied[1].parameters) == 1 + + +class _LegacyMacroArgs(TypedDict): + """Legacy macro fields used together during upgrade checks.""" + + macro_type: str + schema_path: list[str] + parameters: list[str] + definition: str + parameter_default_values: NotRequired[pa.RecordBatch] + arguments_schema: NotRequired[pa.Schema] + + +def test_legacy_upgrade_preserves_typed_null_defaults_without_declaring_their_types() -> None: + defaults = literal("optional", None, metadata={b"source": b"legacy"}).batch + args: _LegacyMacroArgs = dict( + macro_type="scalar", + schema_path=["a.b", "nested"], + parameters=["required", "optional"], + definition="coalesce(optional, required)", + parameter_default_values=defaults, + arguments_schema=macro_arguments_schema(["required", "optional"], defaults, {"optional": "optional doc"}), + ) + upgraded = normalize_macro_overloads(**args, evaluation_context=QueryEvaluationContext()) + assert all(parameter.declared_type is None for parameter in upgraded[0].parameters) + assert upgraded[0].parameters[1].documentation == "optional doc" + restored = legacy_macro_fields(upgraded, evaluation_context=QueryEvaluationContext()) + assert restored["parameter_default_values"].equals(defaults, check_metadata=True) + assert restored["arguments_schema"].equals(args["arguments_schema"], check_metadata=True) + assert restored["parameters"] == args["parameters"] + + +def test_legacy_documentation_schema_cannot_sneak_in_a_declared_type_or_drop_metadata() -> None: + args: _LegacyMacroArgs = dict(macro_type="scalar", schema_path=["main"], parameters=["x"], definition="x") + for schema in [ + pa.schema([pa.field("x", pa.int64())]), + pa.schema([pa.field("x", pa.null())], metadata={b"unknown": b"preserve"}), + ]: + args["arguments_schema"] = schema + with pytest.raises(ValueError, match="unsupported or inconsistent"): + normalize_macro_overloads(**args, evaluation_context=QueryEvaluationContext()) + + +@pytest.mark.parametrize("kind", ["family", "declared", "expression", "empty_doc"]) +def test_legacy_downgrade_fails_closed_instead_of_losing_information(kind: str) -> None: + family = [overload()] + if kind == "family": + family.append(overload([], definition="42")) + elif kind == "declared": + family = [overload([MacroParameter(name="x", declared_type=pa.schema([pa.field("x", pa.int64())]))])] + elif kind == "expression": + family = [ + overload( + [ + MacroParameter( + name="x", + default_expression=SqlExpression( + evaluation_context=QueryEvaluationContext(), + sql="42", + profile=MACRO_DEFAULT_PROFILE, + schema_path=["a.b", "nested"], + ), + ) + ] + ) + ] + else: + family = [overload([MacroParameter(name="x", documentation="")])] + with pytest.raises(ValueError, match="legacy macros"): + legacy_macro_fields(family) + + +def test_macro_ipc_requires_exactly_one_complete_record() -> None: + valid = overload().serialize_to_bytes() + with pytest.raises(ValueError, match="trailing"): + MacroOverload.deserialize_from_bytes(valid + b"extra") + with pa.ipc.open_stream(valid) as reader: + batch = reader.read_next_batch() + with pytest.raises(ValueError, match="complete schema"): + MacroOverload.deserialize_from_batch(batch.select(["parameters"])) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, batch.schema) as writer: + writer.write_batch(batch) + writer.write_batch(batch) + with pytest.raises(ValueError, match="exactly one batch"): + MacroOverload.deserialize_from_bytes(sink.getvalue().to_pybytes()) + + +def test_serialization_revalidates_mutated_parameter_collection() -> None: + value = overload() + value.parameters.append(MacroParameter(name="X")) + with pytest.raises(ValueError, match="unique"): + value.serialize_to_bytes() + + +class _FamilyArgs(TypedDict): + """Common create/discovery fields whose mutable family is revalidated.""" + + name: str + schema_path: list[str] + macro_type: MacroType + overloads: list[MacroOverload] + + +@pytest.mark.parametrize("kind", ["info", "create"]) +def test_parent_serialization_revalidates_mutated_nested_family(kind: str) -> None: + from vgi.catalog import MacroInfo, MacroType, OnConflict + from vgi.protocol import MacroCreateRequest + + member = overload() + common: _FamilyArgs = dict( + name="m", + schema_path=member.schema_path, + macro_type=MacroType.SCALAR, + overloads=[member], + ) + parent = ( + MacroInfo(**common, comment=None, tags={}) + if kind == "info" + else MacroCreateRequest(**common, attach_opaque_data=b"attach", on_conflict=OnConflict.ERROR) + ) + parent.serialize_to_bytes() + member.parameters.append(MacroParameter(name="X")) + with pytest.raises(ValueError, match="unique"): + parent.serialize_to_bytes() + + +def test_create_discovery_copy_resolves_default_once_with_duckdb_15() -> None: + """A finite reference binds a known constant and stores its resolved value.""" + owner = ["main"] + expression = SqlExpression( + evaluation_context=QueryEvaluationContext(), sql="40 + 2", profile=MACRO_DEFAULT_PROFILE, schema_path=owner + ) + family = [ + overload( + [MacroParameter(name="x", default_expression=expression)], + schema_path=owner, + definition="x + 1", + ) + ] + calls = [] + with duckdb.connect() as connection: + + def resolve(item: MacroOverload, parameter: MacroParameter) -> TypedValueBatch: + assert item.schema_path == owner + assert present(parameter.default_expression).sql == "40 + 2" + calls.append(parameter.name) + # This finite reference evaluates only its one whitelisted constant; + # a general provider must use the engine's constant-expression binder. + result = connection.sql('SELECT 40 + 2 AS "x"').arrow().read_next_batch() + return TypedValueBatch(result) + + created = canonicalize_macro_defaults(family, resolve) + stored = [item.serialize_to_bytes() for item in created] + discovered = [MacroOverload.deserialize_from_bytes(data) for data in stored] + copied = normalize_macro_overloads(**{**fields(overloads=discovered), "schema_path": owner}) + assert copied[0].parameters[0].default_expression is None + assert present(copied[0].parameters[0].default_value).batch.column(0)[0].as_py() == 42 + assert calls == ["x"] + assert family[0].parameters[0].default_expression is not None + assert copied[0].serialize_to_bytes() == stored[0] + connection.execute("CREATE MACRO original_macro(x := 40 + 2) AS x + 1") + connection.execute("CREATE MACRO copied_macro(x := 42) AS x + 1") + assert connection.sql("SELECT original_macro(), copied_macro()").fetchone() == (43, 43) + + +def test_default_resolution_failure_does_not_publish_partial_replacement() -> None: + family = [overload([MacroParameter(name="x", default_value=literal(value=1))])] + existing = [item.serialize_to_bytes() for item in family] + + def reject(item: MacroOverload, parameter: MacroParameter) -> TypedValueBatch: + raise ValueError("default cannot be implicitly cast") + + with pytest.raises(ValueError, match="implicitly cast"): + canonicalize_macro_defaults(family, reject) + assert [item.serialize_to_bytes() for item in family] == existing + + +def test_duckdb_15_owns_typed_overload_resolution_and_duplicate_signature_errors() -> None: + with duckdb.connect() as connection: + connection.execute("CREATE MACRO typed_macro(x INTEGER) AS x + 1, (x VARCHAR) AS x || '!' ") + assert connection.sql("SELECT typed_macro(2::INTEGER), typed_macro('v'::VARCHAR)").fetchone() == (3, "v!") + with pytest.raises(duckdb.Error, match="Ambiguity|same parameters"): + connection.execute("CREATE MACRO ambiguous_macro(x INTEGER) AS x, (y INTEGER) AS y + 1") diff --git a/tests/catalog/test_macro_overload_protocol.py b/tests/catalog/test_macro_overload_protocol.py new file mode 100644 index 00000000..fc9f44af --- /dev/null +++ b/tests/catalog/test_macro_overload_protocol.py @@ -0,0 +1,444 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D101, D102, D103 + +"""Mandatory macro families over signed HTTP with durable snapshot transactions.""" + +from __future__ import annotations + +import base64 +import json +import sqlite3 +from collections.abc import Sequence +from contextlib import closing +from dataclasses import replace +from pathlib import Path +from typing import Literal, overload + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.catalog import MacroInfo, MacroOverload, MacroParameter, MacroType, OnConflict, SchemaObjectType +from vgi.catalog.capabilities import CatalogCapabilities +from vgi.catalog.catalog_interface import ( + AttachOpaqueData, + CatalogContentsResult, + FunctionInfo, + IndexInfo, + SchemaInfo, + TableInfo, + TransactionOpaqueData, + ViewInfo, +) +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.layout import SqlExpression +from vgi.catalog.macros import MACRO_DEFAULT_PROFILE, MACRO_PROFILE, canonicalize_macro_defaults +from vgi.client import Client +from vgi.client.catalog_mixin import CatalogClientError +from vgi.protocol import CatalogAttachRequest, MacroCreateRequest +from vgi.serve import create_app +from vgi.typed_values import TypedValueBatch +from vgi.worker import Worker + +from .schema_options_reference import SchemaOptionsCatalog +from .typing_helpers import CatalogObjectArgs, present + +KEY = b"macro-contract-reference-signing-key" + + +class MacroCatalog(SchemaOptionsCatalog): + profiles = CatalogCapabilities() + + @staticmethod + def _macro_key(path: list[str], name: str) -> str: + return "macro:" + json.dumps([path, name]) + + def schemas( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None + ) -> list[SchemaInfo]: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + return [ + self._decode(value, attach_opaque_data) for key, value in data.items() if not key.startswith("macro:") + ] + + def macro_create( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: list[str], + name: str, + macro_type: MacroType, + overloads: list[MacroOverload], + on_conflict: OnConflict, + ) -> None: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + key = self._macro_key(schema_path, name) + if key in data: + if on_conflict == OnConflict.IGNORE: + return + if on_conflict != OnConflict.REPLACE: + raise ValueError("macro already exists") + for overload in overloads: + if overload.evaluation_context != QueryEvaluationContext(): + raise ValueError("finite macro fixture requires default explicit context") + definitions = {"x + 1", "x + y"} if overload.macro_type == "scalar" else {"SELECT x AS value"} + if overload.definition not in definitions: + raise ValueError("finite macro fixture rejects unsupported definition") + + def resolve(overload: MacroOverload, parameter: MacroParameter) -> TypedValueBatch: + if parameter.default_expression is None: + assert parameter.default_value is not None + if ( + parameter.declared_type is not None + and parameter.default_value.schema.field(0).type != parameter.declared_type.field(0).type + ): + raise ValueError("finite fixture rejects unsupported default coercion") + return parameter.default_value + if parameter.default_expression.sql != "40 + 2" or parameter.declared_type is not None: + raise ValueError("finite fixture rejects unsupported default") + return TypedValueBatch(pa.record_batch([pa.array([42], type=pa.int64())], names=[parameter.name])) + + candidate = MacroInfo( + comment=None, + tags={}, + name=name, + schema_path=schema_path, + macro_type=macro_type, + overloads=canonicalize_macro_defaults(overloads, resolve), + ) + data[key] = base64.b64encode(candidate.serialize_to_bytes()).decode() + self._store(db, version + 1, data, transaction_opaque_data) + + @staticmethod + def _store(db: sqlite3.Connection, version: int, data: dict[str, str], tx: TransactionOpaqueData | None) -> None: + if tx is None: + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version, json.dumps(data))) + else: + db.execute("UPDATE transactions SET version=?, data=? WHERE id=?", (version, json.dumps(data), tx)) + + def macro_get( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: list[str], + name: str, + ) -> MacroInfo | None: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + value = data.get(self._macro_key(schema_path, name)) + return MacroInfo.deserialize_from_bytes(base64.b64decode(value)) if value is not None else None + + def macro_drop( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: list[str], + name: str, + ignore_not_found: bool, + ) -> None: + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + key = self._macro_key(schema_path, name) + if key not in data: + if ignore_not_found: + return + raise ValueError("macro not found") + del data[key] + self._store(db, version + 1, data, transaction_opaque_data) + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + type: Literal[SchemaObjectType.TABLE], + ) -> list[TableInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + type: Literal[SchemaObjectType.VIEW], + ) -> list[ViewInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + type: Literal[ + SchemaObjectType.SCALAR_FUNCTION, SchemaObjectType.TABLE_FUNCTION, SchemaObjectType.AGGREGATE_FUNCTION + ], + ) -> list[FunctionInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + type: Literal[SchemaObjectType.SCALAR_MACRO, SchemaObjectType.TABLE_MACRO], + ) -> list[MacroInfo]: ... + + @overload + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + type: Literal[SchemaObjectType.INDEX], + ) -> list[IndexInfo]: ... + + def schema_contents( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: list[str], + type: SchemaObjectType, + ) -> Sequence[TableInfo | ViewInfo | FunctionInfo | MacroInfo | IndexInfo]: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach_opaque_data, transaction_opaque_data) + return [ + info + for key, value in data.items() + if key.startswith("macro:") + if (info := MacroInfo.deserialize_from_bytes(base64.b64decode(value))).schema_path == path + and type + == ( + SchemaObjectType.SCALAR_MACRO + if info.macro_type == MacroType.SCALAR + else SchemaObjectType.TABLE_MACRO + ) + ] + + def catalog_contents( + self, *, attach_opaque_data: AttachOpaqueData, if_none_match: str | None = None + ) -> CatalogContentsResult: + source = super().catalog_contents(attach_opaque_data=attach_opaque_data, if_none_match=if_none_match) + return replace( + source, + schemas=[ + replace( + schema, + scalar_macros=self.schema_contents( + attach_opaque_data=attach_opaque_data, + transaction_opaque_data=None, + path=schema.schema.path, + type=SchemaObjectType.SCALAR_MACRO, + ), + table_macros=self.schema_contents( + attach_opaque_data=attach_opaque_data, + transaction_opaque_data=None, + path=schema.schema.path, + type=SchemaObjectType.TABLE_MACRO, + ), + ) + for schema in source.schemas + ], + ) + + +class MacroWorker(Worker): + catalog_interface = MacroCatalog + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> MacroWorker: + monkeypatch.setattr(MacroCatalog, "database_path", str(tmp_path / "macro.sqlite"), raising=False) + worker = MacroWorker(quiet=True) + worker._signing_key = KEY + return worker + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(create_app(MacroWorker, describe=False, signing_key=KEY)) + ) + + +def family(*, expression: bool = False, macro_type: str = "scalar") -> list[MacroOverload]: + parameter = MacroParameter(name="x") + if expression: + parameter = replace( + parameter, + default_expression=SqlExpression( + evaluation_context=QueryEvaluationContext(), + sql="40 + 2", + profile=MACRO_DEFAULT_PROFILE, + schema_path=["main"], + ), + ) + return [ + MacroOverload( + evaluation_context=QueryEvaluationContext(), + parameters=[parameter], + definition="x + 1" if macro_type == "scalar" else "SELECT x AS value", + profile=MACRO_PROFILE, + schema_path=["main"], + macro_type=macro_type, + ) + ] + + +def attach(worker: MacroWorker) -> AttachOpaqueData: + return worker.catalog_attach( + CatalogAttachRequest(name="options", data_version_spec=None, implementation_version=None) + ).attach_opaque_data + + +def create(worker: MacroWorker, token: AttachOpaqueData, overloads: list[MacroOverload] | None = None) -> None: + worker.catalog_macro_create( + MacroCreateRequest( + attach_opaque_data=token, + schema_path=["main"], + name="m", + macro_type=MacroType.SCALAR, + on_conflict=OnConflict.REPLACE, + overloads=family() if overloads is None else overloads, + ) + ) + + +@pytest.mark.parametrize("operation", ["get", "list", "contents"]) +def test_mandatory_family_read_requires_no_representation_offer(backend: MacroWorker, operation: str) -> None: + token = attach(backend) + create(backend, token) + if operation == "get": + result = backend.catalog_macro_get(token, ["main"], "m").to_optional() + assert result.overloads == family() + elif operation == "list": + assert len(backend.catalog_schema_contents_macros(token, ["main"], SchemaObjectType.SCALAR_MACRO).items) == 1 + else: + assert backend.catalog_contents(token) is not None + + +@pytest.mark.parametrize("macro_type", [MacroType.SCALAR, MacroType.TABLE]) +def test_http_resolves_defaults_once_and_preserves_complete_family(backend: MacroWorker, macro_type: MacroType) -> None: + with client() as c: + token = c.catalog_attach(name="options", data_version_spec=None, implementation_version=None).attach_opaque_data + target: CatalogObjectArgs = dict(attach_opaque_data=token, schema_path=["main"], name="m") + c.macro_create(**target, macro_type=macro_type, overloads=family(expression=True, macro_type=macro_type.value)) + info = present(c.macro_get(**target)) + parameter = info.overloads[0].parameters[0] + assert ( + parameter.default_expression is None and present(parameter.default_value).batch.column(0)[0].as_py() == 42 + ) + listing = c.schema_contents( + attach_opaque_data=token, + path=["main"], + type=(SchemaObjectType.SCALAR_MACRO if macro_type == MacroType.SCALAR else SchemaObjectType.TABLE_MACRO), + ) + assert listing == [info] + with client() as c: + assert c.macro_get(**target) == info + c.macro_create(**target, macro_type=macro_type, overloads=info.overloads, on_conflict=OnConflict.REPLACE) + assert c.macro_get(**target) == info + + +def test_atomic_replacement_rollback_and_reconnect_use_persisted_records(backend: MacroWorker) -> None: + with client() as c: + token = c.catalog_attach(name="options", data_version_spec=None, implementation_version=None).attach_opaque_data + target: CatalogObjectArgs = dict(attach_opaque_data=token, schema_path=["main"], name="m") + c.macro_create(**target, macro_type=MacroType.SCALAR, overloads=family()) + original = present(c.macro_get(**target)) + second = replace( + family()[0], parameters=[MacroParameter(name="x"), MacroParameter(name="y")], definition="x + y" + ) + complete = [family()[0], second] + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.macro_create( + **target, + transaction_opaque_data=tx, + macro_type=MacroType.SCALAR, + overloads=complete, + on_conflict=OnConflict.REPLACE, + ) + assert len(present(c.macro_get(**target, transaction_opaque_data=present(tx))).overloads) == 2 + assert c.macro_get(**target) == original + with client() as c: + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert c.macro_get(**target) == original + bad = replace( + family(expression=True)[0], + parameters=[ + replace( + family(expression=True)[0].parameters[0], + default_expression=replace( + present(family(expression=True)[0].parameters[0].default_expression), sql="random()" + ), + ) + ], + ) + before = c.catalog_version(attach_opaque_data=token) + with pytest.raises(CatalogClientError, match="unsupported default"): + c.macro_create( + **target, macro_type=MacroType.SCALAR, overloads=[second, bad], on_conflict=OnConflict.REPLACE + ) + assert c.macro_get(**target) == original and c.catalog_version(attach_opaque_data=token) == before + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.macro_create( + **target, + transaction_opaque_data=tx, + macro_type=MacroType.SCALAR, + overloads=complete, + on_conflict=OnConflict.REPLACE, + ) + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + with client() as c: + assert present(c.macro_get(**target)).overloads == complete + c.macro_drop(**target) + assert c.macro_get(**target) is None + + +def test_discovery_record_rejects_unresolved_default_expression() -> None: + with pytest.raises(ValueError, match="resolved at creation"): + MacroInfo( + comment=None, + tags={}, + name="m", + schema_path=["main"], + macro_type=MacroType.SCALAR, + overloads=family(expression=True), + ) + + +def test_http_retains_typed_null_docs_and_rejects_unqualified_coercion(backend: MacroWorker) -> None: + declared = pa.schema([pa.field("x", pa.int64(), metadata={b"type_note": b"retained"})]) + value = TypedValueBatch(pa.record_batch([pa.array([None], type=pa.int64())], schema=declared)) + parameter = MacroParameter(name="x", declared_type=declared, default_value=value, documentation="") + member = replace(family()[0], parameters=[parameter]) + with client() as c: + token = c.catalog_attach(name="options", data_version_spec=None, implementation_version=None).attach_opaque_data + target: CatalogObjectArgs = dict(attach_opaque_data=token, schema_path=["main"], name="m") + c.macro_create(**target, macro_type=MacroType.SCALAR, overloads=[member]) + original = present(c.macro_get(**target)) + restored = original.overloads[0].parameters[0] + assert present(restored.declared_type).equals(declared, check_metadata=True) + assert present(restored.default_value).batch.equals(value.batch, check_metadata=True) + assert restored.has_default and restored.documentation == "" + bad = replace(parameter, declared_type=pa.schema([pa.field("x", pa.string())])) + with pytest.raises(CatalogClientError, match="unsupported default coercion"): + c.macro_create( + **target, + macro_type=MacroType.SCALAR, + overloads=[replace(member, parameters=[bad])], + on_conflict=OnConflict.REPLACE, + ) + assert c.macro_get(**target) == original diff --git a/tests/catalog/test_metadata_changes.py b/tests/catalog/test_metadata_changes.py new file mode 100644 index 00000000..5b482f7a --- /dev/null +++ b/tests/catalog/test_metadata_changes.py @@ -0,0 +1,109 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Patch intent, validation and wire shape must survive before any mutation.""" + +import base64 +import json +from typing import Any + +import pyarrow as pa +import pytest + +from vgi.catalog.metadata_changes import CatalogMetadataChanges +from vgi.codegen.catalog_contract_fixtures import render + + +def test_patch_retains_omission_null_empty_and_literal_tags() -> None: + tags = {"a.b": "old", "old": "remove", "OLD": "keep", "": "blank-key"} + patch = CatalogMetadataChanges( + set_comment=True, comment="", set_tags={"a.b": "", "__proto__": "literal"}, remove_tags=["old"] + ) + restored = CatalogMetadataChanges.deserialize_from_bytes(patch.serialize_to_bytes()) + comment, result = restored.apply_to("old", tags) + assert comment == "" and result == {"a.b": "", "OLD": "keep", "": "blank-key", "__proto__": "literal"} + assert tags["old"] == "remove" and tags["a.b"] == "old" + assert CatalogMetadataChanges().apply_to(comment, result) == (comment, result) + assert CatalogMetadataChanges(set_comment=True).apply_to(comment, result) == (None, result) + assert CatalogMetadataChanges(remove_tags=["missing"]).apply_to(comment, result) == (comment, result) + + +@pytest.mark.parametrize( + "kwargs", + [ + {"comment": "lost"}, + {"set_comment": None}, + {"set_comment": 1}, + {"comment": 7}, + {"set_tags": None}, + {"set_tags": {"a": None}}, + {"set_tags": {1: "a"}}, + {"remove_tags": None}, + {"remove_tags": [None]}, + {"remove_tags": ["a", "a"]}, + {"set_tags": {"a": "x"}, "remove_tags": ["a"]}, + ], +) +def test_invalid_patches_reject(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + CatalogMetadataChanges(**kwargs) + + +def test_mutable_patch_members_are_validated_again_at_application() -> None: + patch = CatalogMetadataChanges(set_tags={"a": "x"}) + patch.remove_tags.append("a") + original = {"a": "original"} + with pytest.raises(ValueError, match="SET and REMOVE"): + patch.apply_to("old", original) + assert original == {"a": "original"} + + +@pytest.mark.parametrize( + "kind", ["missing", "wrong_type", "null_flag", "null_map", "null_removals", "duplicate_keys", "two_rows"] +) +def test_wire_rejects_incomplete_or_ambiguous_patches(kind: str) -> None: + schema = CatalogMetadataChanges.ARROW_SCHEMA + row: dict[str, Any] = {"set_comment": True, "comment": "new", "set_tags": [], "remove_tags": []} + if kind == "missing": + schema = schema.remove(schema.get_field_index("set_comment")) + elif kind == "wrong_type": + schema = schema.set(0, pa.field("set_comment", pa.int32(), nullable=False)) + row["set_comment"] = 1 + elif kind.startswith("null_"): + row[{"null_flag": "set_comment", "null_map": "set_tags", "null_removals": "remove_tags"}[kind]] = None + elif kind == "duplicate_keys": + row["set_tags"] = [("a", "one"), ("a", "two")] + batch = pa.RecordBatch.from_pylist([row] * (2 if kind == "two_rows" else 1), schema=schema) + with pytest.raises(ValueError): + CatalogMetadataChanges.deserialize_from_batch(batch) + + +def test_shared_corpus_preserves_patch_intent() -> None: + fixtures = json.loads(render())["records"] + records = { + name: CatalogMetadataChanges.deserialize_from_bytes(base64.b64decode(fixtures[name])) + for name in ("metadata_patch", "metadata_clear_comment", "metadata_noop") + } + assert records["metadata_patch"].comment == "" + assert records["metadata_patch"].set_tags == {"a.b": "", "__proto__": "literal", "": "empty-key"} + assert records["metadata_patch"].remove_tags == ["old", "OLD"] + assert records["metadata_clear_comment"].set_comment and records["metadata_clear_comment"].comment is None + assert not records["metadata_noop"].set_comment + + +@pytest.mark.parametrize("kind", ["no_batch", "extra_batch", "trailing_bytes"]) +def test_patch_ipc_rejects_additional_or_missing_contents(kind: str) -> None: + schema = CatalogMetadataChanges.ARROW_SCHEMA + sink = pa.BufferOutputStream() + batch = pa.RecordBatch.from_pylist( + [{"set_comment": False, "comment": None, "set_tags": [], "remove_tags": []}], schema=schema + ) + with pa.ipc.new_stream(sink, schema) as writer: + if kind != "no_batch": + writer.write_batch(batch) + if kind == "extra_batch": + writer.write_batch(batch) + payload = sink.getvalue().to_pybytes() + if kind == "trailing_bytes": + payload += b"extra" + with pytest.raises(ValueError): + CatalogMetadataChanges.deserialize_from_bytes(payload) diff --git a/tests/catalog/test_nullable_default_schema.py b/tests/catalog/test_nullable_default_schema.py new file mode 100644 index 00000000..ffd13085 --- /dev/null +++ b/tests/catalog/test_nullable_default_schema.py @@ -0,0 +1,195 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Absent catalog defaults survive real attachment and qualified discovery.""" + +from __future__ import annotations + +import json +from typing import Any + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.arguments import Arguments +from vgi.catalog import Catalog, CatalogAttachResult, ReadOnlyCatalogInterface, Schema, SchemaObjectType, View +from vgi.catalog.catalog_interface import AttachOpaqueData +from vgi.client import Client +from vgi.client.cli_utils import catalog_attach_result_to_dict +from vgi.invocation import FunctionType +from vgi.protocol import BindRequest, CatalogAttachRequest +from vgi.serve import create_app +from vgi.table_function import TableFunctionGenerator, bind_fixed_schema +from vgi.worker import Worker + +KEY = b"nullable-default-schema-signing-key" + + +def attach_result(**kwargs: Any) -> CatalogAttachResult: + return CatalogAttachResult( + attach_opaque_data=AttachOpaqueData(b"attachment"), + supports_transactions=False, + supports_time_travel=False, + catalog_version_frozen=True, + catalog_version=1, + resolved_data_version=None, + resolved_implementation_version=None, + **kwargs, + ) + + +@pytest.mark.parametrize("default_schema", [None, "main", "sales.archive"]) +def test_attach_record_round_trip_preserves_optional_raw_default(default_schema: str | None) -> None: + source = attach_result(default_schema=default_schema) + restored = CatalogAttachResult.deserialize_from_bytes(source.serialize_to_bytes()) + assert restored.default_schema == default_schema + assert CatalogAttachResult.ARROW_SCHEMA.field("default_schema").nullable + assert json.loads(json.dumps(catalog_attach_result_to_dict(restored)))["default_schema"] == default_schema + + +def test_omitted_default_still_means_main() -> None: + assert attach_result().default_schema == "main" + assert Catalog(name="legacy", schemas=[Schema(path=["main"])]).default_schema == "main" + assert ReadOnlyCatalogInterface()._default_schema_name == "main" + + +@pytest.mark.parametrize("invalid", ["", "bad\x00name", 42]) +def test_invalid_default_rejects_in_both_public_records(invalid: Any) -> None: + with pytest.raises((TypeError, ValueError), match="default_schema"): + attach_result(default_schema=invalid) + with pytest.raises((TypeError, ValueError), match="default_schema"): + Catalog(name="invalid", default_schema=invalid, schemas=[Schema(path=["main"])]) + + +def test_no_default_allows_empty_catalog_and_dots_do_not_become_nested_paths() -> None: + assert Catalog(name="empty", default_schema=None).schemas == () + with pytest.raises(ValueError, match="not found"): + Catalog( + name="nested", + default_schema="sales.archive", + schemas=[Schema(path=["sales"]), Schema(path=["sales", "archive"])], + ) + + +@pytest.mark.parametrize("default_schema", [None, "main", "sales.archive"]) +@pytest.mark.parametrize("bulk", [False, True]) +def test_signed_http_attach_load_and_qualified_discovery(default_schema: str | None, bulk: bool) -> None: + descriptor = Catalog( + name="optional_default", + default_schema=default_schema, + schemas=[ + Schema(path=["main"], views=[View(name="answer", definition="SELECT 1")]), + Schema(path=["sales.archive"], views=[View(name="answer", definition="SELECT 2")]), + ], + ) + + class Provider(ReadOnlyCatalogInterface): + catalog = descriptor + + def catalog_attach(self, **kwargs: Any) -> CatalogAttachResult: + from dataclasses import replace + + return replace(super().catalog_attach(**kwargs), supports_catalog_contents=bulk) + + class CatalogWorker(Worker): + catalog_interface = Provider + + with Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(create_app(CatalogWorker, describe=False, signing_key=KEY)) + ) as client: + attached = client.catalog_attach(name="optional_default", data_version_spec=None, implementation_version=None) + assert attached.default_schema == default_schema + snapshot = client.load_catalog(attach=attached) + assert snapshot.source == ("catalog_contents" if bulk else "per_schema") + assert {tuple(item.schema.path) for item in snapshot.schemas} == {("main",), ("sales.archive",)} + for path, sql in [(["main"], "SELECT 1"), (["sales.archive"], "SELECT 2")]: + view = client.view_get(attach_opaque_data=attached.attach_opaque_data, schema_path=path, name="answer") + assert view is not None and view.definition == sql + assert client.schema_contents( + attach_opaque_data=attached.attach_opaque_data, path=path, type=SchemaObjectType.VIEW + ) == [view] + assert attached.default_schema == default_schema + assert catalog_attach_result_to_dict(attached)["default_schema"] == default_schema + + +@pytest.mark.parametrize("invalid", ["", "bad\x00name"]) +def test_worker_validates_provider_result_before_publishing_attachment(invalid: str) -> None: + calls = [] + + class Provider(ReadOnlyCatalogInterface): + catalog = Catalog(name="invalid", default_schema=None) + + def catalog_attach(self, **kwargs: Any) -> CatalogAttachResult: + result = attach_result(default_schema=None) + object.__setattr__(result, "default_schema", invalid) + return result + + def catalog_capabilities(self, **kwargs: Any) -> Any: + calls.append("capabilities") + raise AssertionError("invalid attachment must not be published") + + class CatalogWorker(Worker): + catalog_interface = Provider + + worker = CatalogWorker(quiet=True) + worker._signing_key = KEY + with pytest.raises(ValueError, match="default_schema"): + worker.catalog_attach(CatalogAttachRequest(name="invalid", data_version_spec=None, implementation_version=None)) + assert calls == [] + + +@bind_fixed_schema +class Probe(TableFunctionGenerator[None]): + """Minimal registered function used to qualify schema routing.""" + + class Meta: + """Declare the function name used by the routing probes.""" + + name = "probe" + + FIXED_SCHEMA = pa.schema([pa.field("value", pa.int64())]) + + +def test_no_default_rejects_functions_without_explicit_schema() -> None: + class CatalogWorker(Worker): + catalog = Catalog(name="unscoped", default_schema=None, schemas=[Schema(path=["main"])]) + functions = [Probe] + + with pytest.raises(ValueError, match="explicit schema registration"): + CatalogWorker._build_registry() + + +def test_no_default_keeps_explicit_function_registration() -> None: + class CatalogWorker(Worker): + catalog = Catalog( + name="scoped", + default_schema=None, + schemas=[Schema(path=["sales.archive"], functions=[Probe])], + ) + functions = [Probe] + + worker = CatalogWorker(quiet=True) + assert worker._resolve_function_by_name("probe", schema_path=["sales.archive"]) is Probe + assert (("sales.archive",), "probe") in worker._build_schema_registry() + assert not any(path == ("main",) for path, _ in worker._build_schema_registry()) + + +def test_no_default_keeps_ambiguous_function_names_qualified() -> None: + class OtherProbe(Probe): + pass + + class CatalogWorker(Worker): + catalog = Catalog( + name="scoped", + default_schema=None, + schemas=[Schema(path=["first"], functions=[Probe]), Schema(path=["second"], functions=[OtherProbe])], + ) + + worker = CatalogWorker(quiet=True) + assert worker._resolve_function_by_name("probe", schema_path=["first"]) is Probe + assert worker._resolve_function_by_name("probe", schema_path=["second"]) is OtherProbe + with pytest.raises(ValueError, match="[Aa]mbiguous|multiple|schema"): + worker._resolve_function( + BindRequest(function_name="probe", arguments=Arguments(), function_type=FunctionType.TABLE) + ) diff --git a/tests/catalog/test_options.py b/tests/catalog/test_options.py new file mode 100644 index 00000000..36d6d8d5 --- /dev/null +++ b/tests/catalog/test_options.py @@ -0,0 +1,185 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Typed option changes preserve readback and reject the entire invalid change.""" + +from decimal import Decimal +from typing import Any + +import pyarrow as pa +import pytest + +from vgi.catalog.options import OptionChanges, OptionState +from vgi.typed_values import TypedValueBatch + + +def _defaults() -> TypedValueBatch: + fields: list[pa.Field[Any]] = [ + pa.field("label", pa.string(), metadata={b"option": b"display label"}), + pa.field("limit", pa.int32(), nullable=False), + pa.field("cost", pa.decimal128(12, 3)), + pa.field( + "nested", pa.struct([pa.field("enabled", pa.bool_(), nullable=False, metadata={b"child": b"retained"})]) + ), + ] + schema = pa.schema(fields, metadata={b"provider": b"example-options/v1"}) + return TypedValueBatch( + pa.RecordBatch.from_pylist( + [{"label": "default", "limit": 10, "cost": Decimal("1.125"), "nested": {"enabled": True}}], schema=schema + ) + ) + + +def _sets(**values: Any) -> TypedValueBatch: + defaults = _defaults() + schema = pa.schema( + [defaults.schema.field(name) for name in values], + metadata=None if defaults.schema.metadata is None else {k: v for k, v in defaults.schema.metadata.items()}, + ) + return TypedValueBatch(pa.RecordBatch.from_pylist([values], schema=schema)) + + +def test_set_null_omission_and_reset_have_distinct_readback() -> None: + defaults = _defaults() + original = OptionState(values=defaults) + changed = OptionChanges(set_values=_sets(label=None, limit=20)).apply_to(original, defaults=defaults) + assert changed.values.batch.to_pylist()[0] == { + "label": None, + "limit": 20, + "cost": Decimal("1.125"), + "nested": {"enabled": True}, + } + assert changed.explicit_names == ["label", "limit"] + assert original.values.batch.column("limit")[0].as_py() == 10 + assert original.explicit_names == [] + # An empty change leaves both effective values and the overrides intact. + unchanged = OptionChanges().apply_to(changed, defaults=defaults) + assert unchanged.values.batch.equals(changed.values.batch, check_metadata=True) + assert unchanged.explicit_names == changed.explicit_names + reset = OptionChanges(reset_names=["label"]).apply_to(changed, defaults=defaults) + assert reset.values.batch.column("label")[0].as_py() == "default" + assert reset.values.batch.column("limit")[0].as_py() == 20 + assert reset.explicit_names == ["limit"] + + +def test_setting_default_value_still_creates_an_override() -> None: + defaults = _defaults() + result = OptionChanges(set_values=_sets(limit=10)).apply_to(OptionState(values=defaults), defaults=defaults) + assert result.values.batch.equals(defaults.batch, check_metadata=True) + assert result.explicit_names == ["limit"] + assert OptionChanges(reset_names=["limit"]).apply_to(result, defaults=defaults).explicit_names == [] + + +def test_reset_uses_the_current_provider_default() -> None: + original = OptionState(values=_defaults()) + current_defaults = OptionChanges(set_values=_sets(limit=99)).apply_to(original, defaults=_defaults()).values + result = OptionChanges(reset_names=["limit"]).apply_to(original, defaults=current_defaults) + assert result.values.batch.column("limit")[0].as_py() == 99 + assert result.explicit_names == [] + + +def test_wire_round_trip_preserves_nested_metadata_and_typed_null() -> None: + changes = OptionChanges(set_values=_sets(cost=None, nested={"enabled": False}), reset_names=["limit"]) + restored = OptionChanges.deserialize_from_bytes(changes.serialize_to_bytes()) + assert restored.set_values is not None and changes.set_values is not None + assert restored.set_values.batch.equals(changes.set_values.batch, check_metadata=True) + assert restored.reset_names == ["limit"] + result = restored.apply_to(OptionState(values=_defaults()), defaults=_defaults()) + readback = OptionState.deserialize_from_bytes(result.serialize_to_bytes()) + assert readback.values.batch.equals(result.values.batch, check_metadata=True) + assert readback.explicit_names == ["cost", "nested"] + + +@pytest.mark.parametrize("names", [["limit", "limit"], [""], ["bad\x00name"]]) +def test_invalid_names_reject(names: list[str]) -> None: + with pytest.raises(ValueError): + OptionChanges(reset_names=names) + with pytest.raises(ValueError): + OptionState(values=_defaults(), explicit_names=names) + + +def test_unknown_explicit_name_rejects() -> None: + with pytest.raises(ValueError, match="absent from values"): + OptionState(values=_defaults(), explicit_names=["missing"]) + + +def test_set_reset_overlap_rejects() -> None: + with pytest.raises(ValueError, match="both SET and RESET"): + OptionChanges(set_values=_sets(label=None), reset_names=["label"]) + + +@pytest.mark.parametrize( + "changes", + [ + OptionChanges(reset_names=["missing"]), + OptionChanges(set_values=TypedValueBatch(pa.record_batch({"missing": [1]}))), + OptionChanges(set_values=TypedValueBatch(pa.record_batch({"limit": [20]}))), + OptionChanges(set_values=_sets(limit=20), reset_names=["missing"]), + ], +) +def test_invalid_changes_leave_original_state_unchanged(changes: OptionChanges) -> None: + defaults = _defaults() + current = OptionState(values=defaults) + before = current.serialize_to_bytes() + with pytest.raises(ValueError, match="unknown option|expected schema"): + changes.apply_to(current, defaults=defaults) + assert current.serialize_to_bytes() == before + + +def test_read_only_options_reject_before_candidate_application() -> None: + current = OptionState(values=_defaults()) + changes = OptionChanges(set_values=_sets(label="would change", limit=20)) + with pytest.raises(ValueError, match="not mutable.*limit"): + changes.apply_to(current, defaults=_defaults(), mutable_names=["label"]) + assert current.values.batch.column("label")[0].as_py() == "default" + + +@pytest.mark.parametrize("part", ["field", "schema", "nested", "nullability"]) +def test_semantic_schema_changes_cannot_be_silently_cast(part: str) -> None: + defaults = _defaults() + field = defaults.schema.field("nested") + metadata = defaults.schema.metadata + if part == "field": + field = field.with_metadata({b"encoding": b"unknown"}) + elif part == "schema": + metadata = {b"provider": b"different"} + elif part == "nested": + field = field.with_type(pa.struct([field.type[0].with_metadata({b"child": b"changed"})])) + else: + field = field.with_nullable(False) + values = TypedValueBatch( + pa.RecordBatch.from_pylist( + [{"nested": {"enabled": False}}], + schema=pa.schema([field], metadata=None if metadata is None else {k: v for k, v in metadata.items()}), + ) + ) + with pytest.raises(ValueError, match="expected schema"): + OptionChanges(set_values=values).apply_to(OptionState(values=defaults), defaults=defaults) + + +def test_empty_option_provider_retains_one_row_and_metadata() -> None: + defaults = TypedValueBatch.empty(metadata={b"provider": b"no-options"}) + current = OptionState(values=defaults) + result = OptionChanges().apply_to(current, defaults=defaults) + assert result.values.batch.equals(defaults.batch, check_metadata=True) + + +def test_mutated_lists_are_revalidated_before_application() -> None: + current = OptionState(values=_defaults()) + changes = OptionChanges(set_values=_sets(limit=20)) + changes.reset_names.append("limit") + with pytest.raises(ValueError, match="both SET and RESET"): + changes.apply_to(current, defaults=_defaults()) + + +def test_nested_option_payload_rejects_extra_batches() -> None: + values = _sets(limit=20) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, values.schema) as writer: + writer.write_batch(values.batch) + writer.write_batch(values.batch) + envelope = pa.RecordBatch.from_pylist( + [{"set_values": sink.getvalue().to_pybytes(), "reset_names": []}], schema=OptionChanges.ARROW_SCHEMA + ) + with pytest.raises(ValueError, match="exactly one batch"): + OptionChanges.deserialize_from_bytes(TypedValueBatch(envelope).serialize_to_bytes()) diff --git a/tests/catalog/test_query_contract.py b/tests/catalog/test_query_contract.py new file mode 100644 index 00000000..8e541392 --- /dev/null +++ b/tests/catalog/test_query_contract.py @@ -0,0 +1,300 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Mode, negotiation, output and schema invariants before remote execution.""" + +from dataclasses import replace +from typing import Any + +import pyarrow as pa +import pytest + +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.query import ( + QUERY_AUTOMATIC, + QUERY_ENCODING, + QUERY_EXPLICIT, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, + QueryEvaluationContext, + QueryObjectReference, +) +from vgi.query_parameters import QueryParameters + +from .typing_helpers import present + +OUTPUT = pa.schema([pa.field("result", pa.int32())]).serialize().to_pybytes() +PARAMETERS = QueryParameters.from_batch( + pa.record_batch([pa.array([None], type=pa.int32())], names=["1"]), mode="positional" +) + + +def capability() -> QueryCapabilities: + return QueryCapabilities( + encoding=QUERY_ENCODING, + semantic_profiles=[QUERY_EXPLICIT, QUERY_AUTOMATIC], + execution_modes=["explicit", "automatic"], + backend_engine="duckdb", + backend_version="pinned-test-build", + capability_revision="policy-1", + default_evaluation_context=QueryEvaluationContext(), + ) + + +def request( + *, expected_output_schema: bytes | None = None, required_ordering: str = "unspecified" +) -> CatalogQueryPrepareRequest: + return CatalogQueryPrepareRequest( + attach_opaque_data=b"attach", + encoding=QUERY_ENCODING, + semantic_profile=QUERY_EXPLICIT, + mode="explicit", + capability_revision="policy-1", + query="SELECT $1 AS result", + parameter_schema=PARAMETERS.schema_bytes(), + evaluation_context=QueryEvaluationContext(), + expected_output_schema=expected_output_schema, + required_ordering=required_ordering, + ) + + +def result( + req: CatalogQueryPrepareRequest | None = None, *, validated_catalog_version: int | None = None +) -> CatalogQueryPrepareResult: + req = req or request() + return CatalogQueryPrepareResult( + status="accepted", + prepared_query_token=b"prepared-recipe", + output_schema=OUTPUT, + result_ordering="query_order", + context_digest=req.evaluation_context.digest(req.encoding, req.semantic_profile), + expires_at_epoch_ms=2_000_000_000_000, + validated_catalog_version=validated_catalog_version, + ) + + +def table_ref(path: list[str] | None = None) -> QueryObjectReference: + return QueryObjectReference( + object=CatalogObjectRef(kind="table", schema_path=path or ["a.b", "nested"], name="T"), + expected_schema=OUTPUT, + ) + + +def automatic() -> CatalogQueryPrepareRequest: + return replace( + request(), + mode="automatic", + semantic_profile=QUERY_AUTOMATIC, + transaction_opaque_data=b"tx", + catalog_version=0, + referenced_objects=[table_ref()], + expected_output_schema=OUTPUT, + ) + + +def test_context_digest_golden_vector_is_independent_of_arrow_layout() -> None: + context = QueryEvaluationContext() + expected = bytes.fromhex("0d33dfb516d67c8096fb174f0ae08a04f028ad5669e4416cde2973a2dd352d34") + assert context.digest(QUERY_ENCODING, QUERY_EXPLICIT) == expected + copy = QueryEvaluationContext.deserialize_from_bytes(context.serialize_to_bytes()) + assert copy.digest(QUERY_ENCODING, QUERY_EXPLICIT) == expected + for changed in [replace(context, preserve_insertion_order=False), replace(context, timezone="Etc/UTC")]: + assert changed.digest(QUERY_ENCODING, QUERY_EXPLICIT) != expected + assert context.digest(QUERY_ENCODING, QUERY_AUTOMATIC) != expected + + +def test_absent_empty_and_unknown_offers_select_no_query_execution() -> None: + supported = capability() + assert supported.select(None, supports_transactions=True) is None + for encodings, profiles in [ + ([], []), + (["other.sql.v1"], [QUERY_EXPLICIT]), + ([QUERY_ENCODING], ["other.profile.v1"]), + ]: + offer = ClientQueryCapabilities(encodings=encodings, semantic_profiles=profiles, engine_version="client-build") + assert supported.select(offer, supports_transactions=True) is None + + +def test_selection_limits_modes_and_size_without_inferring_engine_equivalence() -> None: + supported = capability() + offer = ClientQueryCapabilities( + encodings=[QUERY_ENCODING], + semantic_profiles=[QUERY_EXPLICIT, QUERY_AUTOMATIC], + engine_version="client-build", + max_query_bytes=20, + ) + selected = supported.select(offer, supports_transactions=False) + assert selected is not None + assert selected.execution_modes == ["explicit"] + assert selected.semantic_profiles == [QUERY_EXPLICIT] + assert selected.max_query_bytes == 20 + supported = replace(supported, semantic_profiles=[QUERY_EXPLICIT], execution_modes=["explicit"]) + assert present(supported.select(offer, supports_transactions=True)).execution_modes == ["explicit"] + automatic_only = replace(offer, semantic_profiles=[QUERY_AUTOMATIC]) + assert capability().select(automatic_only, supports_transactions=False) is None + + +@pytest.mark.parametrize( + "change", + [ + {"encoding": "unknown"}, + {"semantic_profiles": ["unknown"]}, + {"execution_modes": ["automatic"]}, + {"execution_modes": ["explicit", "automatic", "write"]}, + {"capability_revision": ""}, + {"max_query_bytes": 0}, + {"max_query_bytes": True}, + {"backend_version": "bad\0version"}, + ], +) +def test_malformed_selected_capability_rejects(change: dict[str, Any]) -> None: + with pytest.raises(ValueError): + replace(capability(), **change) + + +def test_preparation_validates_the_immutable_attachment_contract() -> None: + selected = capability() + source = request() + source.validate_agreement(selected) + for changed in [ + replace(selected, capability_revision="changed"), + replace(selected, max_query_bytes=4), + replace(selected, default_evaluation_context=QueryEvaluationContext(timezone="Etc/UTC")), + replace(selected, semantic_profiles=[QUERY_AUTOMATIC], execution_modes=["automatic"]), + ]: + with pytest.raises(ValueError): + source.validate_agreement(changed) + + +def test_nested_records_keep_raw_identities_in_struct_fields() -> None: + source = automatic() + value = CatalogQueryPrepareRequest.deserialize_from_bytes(source.serialize_to_bytes()) + assert value == source + assert value.referenced_objects[0].object.schema_path == ["a.b", "nested"] + field = source.ARROW_SCHEMA.field("referenced_objects") + assert pa.types.is_struct(field.type.value_type) + assert pa.types.is_struct(field.type.value_type.field("object").type) + assert value.catalog_version == 0 # Unknown metadata is not "unchanged". + + +@pytest.mark.parametrize( + "change", + [ + {"transaction_opaque_data": None}, + {"transaction_opaque_data": b""}, + {"catalog_version": None}, + {"catalog_version": -1}, + {"expected_output_schema": None}, + {"referenced_objects": []}, + {"referenced_objects": [table_ref(), table_ref()]}, + { + "referenced_objects": [ + replace(table_ref(), object=CatalogObjectRef(kind="view", schema_path=["main"], name="v")) + ] + }, + ], +) +def test_automatic_context_requires_complete_snapshot_and_output_contract(change: dict[str, Any]) -> None: + with pytest.raises(ValueError): + replace(automatic(), **change) + + +@pytest.mark.parametrize( + "change", + [ + {"attach_opaque_data": b""}, + {"query": ""}, + {"query": "SELECT\0 1"}, + {"query": "é" * (1024 * 1024)}, + {"parameter_schema": PARAMETERS.serialize_to_bytes()}, + {"referenced_objects": [table_ref()]}, + {"expected_output_schema": OUTPUT + b"extra"}, + {"required_ordering": "ordered_maybe"}, + {"mode": "automatic"}, + ], +) +def test_invalid_preparation_rejects_before_provider_binding(change: dict[str, Any]) -> None: + with pytest.raises(ValueError): + replace(request(), **change) + + +def test_decline_is_exclusive_and_future_reason_codes_remain_pre_execution() -> None: + declined = CatalogQueryPrepareResult( + status="declined", reason_code="future_placement_policy", reason_detail="Not placed" + ) + declined.validate_request(request()) + assert CatalogQueryPrepareResult.deserialize_from_bytes(declined.serialize_to_bytes()) == declined + for field, value in [ + ("prepared_query_token", b"x"), + ("output_schema", OUTPUT), + ("estimated_rows", 0), + ("context_digest", b"x" * 32), + ("validated_catalog_version", 0), + ("expires_at_epoch_ms", 5), + ]: + with pytest.raises(ValueError, match="acceptance fields"): + invalid: dict[str, Any] = {field: value} + replace(declined, **invalid) + with pytest.raises(ValueError, match="reason code"): + replace(declined, reason_code=None) + + +@pytest.mark.parametrize( + "change", + [ + {"status": "unknown"}, + {"reason_code": "bad"}, + {"prepared_query_token": b""}, + {"prepared_query_token": None}, + {"output_schema": None}, + {"output_schema": OUTPUT + b"extra"}, + {"result_ordering": None}, + {"context_digest": b"too short"}, + {"expires_at_epoch_ms": 0}, + {"expires_at_epoch_ms": True}, + {"estimated_bytes": -1}, + ], +) +def test_incomplete_or_contradictory_acceptance_rejects(change: dict[str, Any]) -> None: + with pytest.raises(ValueError): + replace(result(), **change) + + +def test_accepted_context_schema_and_order_are_mandatory_checks() -> None: + source = request(expected_output_schema=OUTPUT, required_ordering="query_order") + accepted = result(source) + accepted.validate_request(source) + for changed in [ + replace(accepted, context_digest=b"x" * 32), + replace(accepted, result_ordering="unspecified"), + replace(accepted, output_schema=pa.schema([pa.field("result", pa.int64())]).serialize().to_pybytes()), + replace(accepted, output_schema=pa.schema([pa.field("renamed", pa.int32())]).serialize().to_pybytes()), + replace( + accepted, + output_schema=pa.schema([pa.field("result", pa.int32(), metadata={b"logical": b"changed"})]) + .serialize() + .to_pybytes(), + ), + ]: + with pytest.raises(ValueError): + changed.validate_request(source) + source = automatic() + with pytest.raises(ValueError, match="metadata version"): + result(source).validate_request(source) + result(source, validated_catalog_version=0).validate_request(source) + + +def test_mutated_nested_lists_and_malformed_outer_records_revalidate() -> None: + source = automatic() + source.referenced_objects.append(table_ref()) + with pytest.raises(ValueError, match="duplicate"): + source.validate_agreement(capability()) + value = result() + batch = pa.ipc.open_stream(value.serialize_to_bytes()).read_next_batch() + with pytest.raises(ValueError, match="complete schema"): + CatalogQueryPrepareResult.deserialize_from_batch(batch.slice(0, 0)) + with pytest.raises(ValueError, match="complete schema"): + CatalogQueryPrepareResult.deserialize_from_batch(batch.drop_columns(["reason_detail"])) + with pytest.raises(ValueError, match="trailing"): + CatalogQueryPrepareResult.deserialize_from_bytes(value.serialize_to_bytes() + b"extra") diff --git a/tests/catalog/test_query_parameters.py b/tests/catalog/test_query_parameters.py new file mode 100644 index 00000000..08364f0c --- /dev/null +++ b/tests/catalog/test_query_parameters.py @@ -0,0 +1,205 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Parameter mapping and exact rebind boundaries in the Arrow batch carrier.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import replace +from typing import Any, Literal, cast + +import pyarrow as pa +import pytest + +from vgi.query_parameters import ( + PARAMETER_MODE_KEY, + QueryParameters, + read_parameter_schema, + validate_parameter_schema, +) +from vgi.typed_values import TypedValueBatch + + +def parameters(value: int | None = None) -> QueryParameters: + fields = [ + pa.field("limit", pa.int32(), metadata={b"logical": b"integer"}), + pa.field( + "record", + pa.struct([pa.field("a.b", pa.string(), nullable=False, metadata={b"logical": b"literal-name"})]), + nullable=False, + ), + ] + batch = pa.RecordBatch.from_pylist( + [{"limit": value, "record": {"a.b": "kept"}}], schema=pa.schema(fields, metadata={b"provider": b"query"}) + ) + return QueryParameters.from_batch(batch, mode="named") + + +def test_typed_null_values_and_recursive_metadata_survive_rebinding() -> None: + prepared = parameters().schema_bytes() + expected = read_parameter_schema(prepared) + for value in [None, 7, -4]: + source = parameters(value) + result = QueryParameters.deserialize_from_bytes(source.serialize_to_bytes()) + result.validate_prepared_schema(prepared) + assert result.schema.equals(expected, check_metadata=True) + assert result.batch.column("limit")[0].as_py() == value + assert result.batch.column("record")[0].as_py() == {"a.b": "kept"} + assert result.mode == "named" + + +@pytest.mark.parametrize("mode", ["named", "positional"]) +def test_zero_parameters_are_one_row_and_mode_is_retained(mode: Literal["named", "positional"]) -> None: + value = QueryParameters.empty(mode=mode) + copy = QueryParameters.deserialize_from_bytes(value.serialize_to_bytes()) + assert copy.batch.num_rows == 1 + assert copy.batch.num_columns == 0 + assert copy.mode == mode + copy.validate_prepared_schema(value.schema_bytes()) + + +def test_positional_fields_map_explicitly_without_casts() -> None: + value = QueryParameters.from_batch( + pa.record_batch([pa.array([None], type=pa.int16()), pa.array([b"value"], type=pa.binary())], names=["1", "2"]), + mode="positional", + ) + assert value.mode == "positional" + assert value.schema.field(0).type == pa.int16() + assert value.batch.column(0)[0].as_py() is None + assert value.batch.column(1)[0].as_py() == b"value" + + +@pytest.mark.parametrize( + ("mode", "names"), + [ + ("positional", ["0"]), + ("positional", ["1", "3"]), + ("positional", ["2", "1"]), + ("positional", ["01"]), + ("positional", ["name"]), + ("named", ["1"]), + ("named", ["$name"]), + ("named", [":name"]), + ("named", ["?name"]), + ("named", ["@name"]), + ("named", ["name", "name"]), + ("named", [""]), + ("named", ["bad\0name"]), + ], +) +def test_invalid_placeholder_mapping_rejects(mode: str, names: list[str]) -> None: + schema = pa.schema([pa.field(n, pa.int32()) for n in names], metadata={PARAMETER_MODE_KEY: mode.encode()}) + with pytest.raises(ValueError): + validate_parameter_schema(schema) + + +@pytest.mark.parametrize("value", [None, b"", b"NAMED", b"unknown"]) +def test_absent_or_unknown_mapping_is_not_guessed(value: bytes | None) -> None: + schema = pa.schema([pa.field("n", pa.int64())], metadata=None if value is None else {PARAMETER_MODE_KEY: value}) + with pytest.raises(ValueError, match="requires"): + validate_parameter_schema(schema) + + +def test_named_spelling_is_preserved_for_provider_resolution() -> None: + # Whether these names are equivalent depends on the selected SQL dialect. + schema = pa.schema([pa.field(n, pa.int32()) for n in ["Name", "name", "naïve"]]) + value = QueryParameters.from_batch(pa.record_batch([[1], [2], [3]], schema=schema), mode="named") + assert value.schema.names == ["Name", "name", "naïve"] + + +@pytest.mark.parametrize("change", ["type", "name", "order", "nullability", "schema_metadata", "nested_metadata"]) +def test_changed_schema_requires_new_preparation(change: str) -> None: + source = parameters(7) + expected = source.schema + if change == "type": + expected = expected.set(0, expected.field(0).with_type(pa.int64())) + elif change == "name": + expected = expected.set(0, expected.field(0).with_name("LIMIT")) + elif change == "order": + expected = pa.schema( + list(reversed(list(expected))), metadata={k: v for k, v in (expected.metadata or {}).items()} + ) + elif change == "nullability": + expected = expected.set(0, expected.field(0).with_nullable(False)) + elif change == "schema_metadata": + expected = expected.with_metadata({**expected.metadata, b"provider": b"changed"}) + else: + nested = expected.field(1) + expected = expected.set( + 1, nested.with_type(pa.struct([nested.type.field(0).with_metadata({b"logical": b"other"})])) + ) + with pytest.raises(ValueError, match="expected schema"): + source.validate_prepared_schema(expected) + + +def test_conflicting_mapping_and_missing_typed_values_reject() -> None: + source = parameters() + with pytest.raises(ValueError, match="conflicts"): + QueryParameters.from_batch(source.batch, mode="positional") + with pytest.raises(TypeError, match="TypedValueBatch"): + replace(source, values=cast(TypedValueBatch, source.batch)) + with pytest.raises(ValueError, match="exactly one row"): + QueryParameters.from_batch(source.batch.slice(0, 0), mode="named") + + +@pytest.mark.parametrize("location", ["schema", "field", "nested", "dictionary"]) +def test_duplicate_metadata_cannot_ambiguously_change_interpretation(location: str) -> None: + metadata = cast("Callable[..., pa.KeyValueMetadata]", pa.KeyValueMetadata)([("logical", "one"), ("logical", "two")]) + field = cast("Callable[..., pa.Field[Any]]", pa.field)("n", pa.int32(), metadata=metadata) + if location == "nested": + field = pa.field("n", pa.list_(field)) + elif location == "dictionary": + field = pa.field("n", cast("Callable[..., pa.DataType]", pa.dictionary)(pa.int8(), pa.struct([field]))) + if location == "schema": + schema = cast("Callable[..., pa.Schema]", pa.schema)( + [], + metadata=cast("Callable[..., pa.KeyValueMetadata]", pa.KeyValueMetadata)( + [(PARAMETER_MODE_KEY, b"named"), (PARAMETER_MODE_KEY, b"positional")] + ), + ) + else: + schema = pa.schema([field], metadata={PARAMETER_MODE_KEY: b"named"}) + with pytest.raises(ValueError, match="metadata keys must be unique"): + validate_parameter_schema(schema) + + +def test_schema_ipc_is_not_a_value_batch_and_both_reject_trailing_data() -> None: + source = parameters() + for suffix in [b"extra", source.schema_bytes()]: + with pytest.raises(ValueError, match="trailing"): + read_parameter_schema(source.schema_bytes() + suffix) + with pytest.raises(ValueError, match="trailing"): + QueryParameters.deserialize_from_bytes(source.serialize_to_bytes() + suffix) + with pytest.raises(ValueError, match="trailing"): + read_parameter_schema(source.serialize_to_bytes()) + with pytest.raises(ValueError, match="no batch"): + QueryParameters.deserialize_from_bytes(source.schema_bytes()) + + +def test_builder_rejects_duplicate_mapping_keys_before_copying_metadata() -> None: + schema = cast("Callable[..., pa.Schema]", pa.schema)( + [pa.field("n", pa.int32())], + metadata=cast("Callable[..., pa.KeyValueMetadata]", pa.KeyValueMetadata)( + [(PARAMETER_MODE_KEY, b"named"), (PARAMETER_MODE_KEY, b"positional")] + ), + ) + with pytest.raises(ValueError, match="metadata keys must be unique"): + QueryParameters.from_batch(pa.record_batch([[1]], schema=schema), mode="named") + + +def test_size_and_parameter_count_limits(monkeypatch: pytest.MonkeyPatch) -> None: + source = parameters() + monkeypatch.setattr("vgi.query_parameters.MAX_PARAMETERS", 1) + with pytest.raises(ValueError, match="too many"): + validate_parameter_schema(source.schema) + monkeypatch.setattr("vgi.query_parameters.MAX_PARAMETERS", 1024) + monkeypatch.setattr("vgi.query_parameters.MAX_PARAMETER_SCHEMA_BYTES", 1) + with pytest.raises(ValueError, match="size"): + read_parameter_schema(source.schema.serialize().to_pybytes()) + with pytest.raises(ValueError, match="too large"): + source.schema_bytes() + monkeypatch.setattr("vgi.query_parameters.MAX_PARAMETER_BYTES", 1) + with pytest.raises(ValueError, match="size"): + QueryParameters.deserialize_from_bytes(b"too large") + with pytest.raises(ValueError, match="too large"): + source.serialize_to_bytes() diff --git a/tests/catalog/test_query_protocol.py b/tests/catalog/test_query_protocol.py new file mode 100644 index 00000000..42e89d87 --- /dev/null +++ b/tests/catalog/test_query_protocol.py @@ -0,0 +1,440 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Public HTTP negotiation, typed execution, ownership and portable result lifecycle.""" + +import sqlite3 +from contextlib import closing +from dataclasses import replace +from pathlib import Path +from typing import Any, cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient +from vgi_rpc.rpc import AuthContext, CallContext + +from vgi import attach_capabilities +from vgi.arguments import Arguments +from vgi.catalog import CatalogAttachResult +from vgi.catalog.capabilities import CatalogCapabilities +from vgi.catalog.query import ( + QUERY_ENCODING, + QUERY_EXECUTOR, + QUERY_EXPLICIT, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + QueryEvaluationContext, +) +from vgi.client import Client +from vgi.invocation import FunctionType +from vgi.meta_worker import MetaWorker +from vgi.protocol import BindRequest, CatalogAttachRequest, ClientCapabilities, InitRequest, VgiProtocol +from vgi.query_parameters import QueryParameters +from vgi.serve import create_app + +from .query_reference import OFFER, QueryReferenceCatalog, QueryReferenceProvider, QueryReferenceWorker +from .typing_helpers import present + +KEY = b"query-reference-signing-key" +CTX = CallContext(auth=AuthContext.anonymous(), emit_client_log=lambda *args, **kwargs: None) +CAPS = ClientCapabilities( + engine="fixture", native_formats=[], catalogs=[], can_stream=True, filter_encodings=[], query_pushdown=OFFER +) + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> QueryReferenceWorker: + monkeypatch.setattr(QueryReferenceCatalog, "database_path", str(tmp_path / "query.sqlite"), raising=False) + worker = QueryReferenceWorker(quiet=True) + worker._signing_key = KEY + return worker + + +def client(attach: bytes | None = None) -> Client: + return Client.from_http( + "http://vgi.test", + attach_opaque_data=attach, + httpx_client=_SyncTestClient(create_app(QueryReferenceWorker, describe=False, signing_key=KEY)), + ) + + +def attach(c: Client, capabilities: ClientCapabilities = CAPS) -> CatalogAttachResult: + return c.catalog_attach( + name="queries", data_version_spec=None, implementation_version=None, client_capabilities=capabilities + ) + + +def parameters(value: int | str | None = 3, name: str = "value", dtype: pa.DataType | None = None) -> QueryParameters: + return QueryParameters.from_batch( + pa.record_batch([pa.array([value], type=dtype or pa.int32())], names=[name]), + mode="positional" if name == "1" else "named", + ) + + +def request( + attached: CatalogAttachResult, + values: QueryParameters | None = None, + query: str = "SELECT $value AS result", + tx: bytes | None = None, +) -> CatalogQueryPrepareRequest: + return CatalogQueryPrepareRequest( + attach_opaque_data=attached.attach_opaque_data, + encoding=QUERY_ENCODING, + semantic_profile=QUERY_EXPLICIT, + mode="explicit", + capability_revision="finite-reference-1", + query=query, + parameter_schema=(values or parameters()).schema_bytes(), + evaluation_context=QueryEvaluationContext(), + transaction_opaque_data=tx, + required_ordering="query_order", + ) + + +def bind_request( + attached: CatalogAttachResult, + prepared: CatalogQueryPrepareResult, + values: QueryParameters | None = None, + tx: bytes | None = None, +) -> BindRequest: + return BindRequest( + function_name=QUERY_EXECUTOR, + schema_path=[], + function_type=FunctionType.TABLE, + attach_opaque_data=attached.attach_opaque_data, + transaction_opaque_data=tx, + arguments=Arguments( + positional=( + pa.scalar(prepared.prepared_query_token, type=pa.binary()), + pa.scalar((values or parameters()).serialize_to_bytes(), type=pa.binary()), + ) + ), + ) + + +def counts() -> dict[str, int]: + with closing(sqlite3.connect(QueryReferenceCatalog.database_path)) as db: + return { + table: db.execute(f"SELECT count(*) FROM query_{table}").fetchone()[0] + for table in ["preparations", "executions", "fragments"] + } | {"starts": db.execute("SELECT starts FROM query_audit").fetchone()[0]} + + +def test_query_agreement_is_optional_private_and_portable(backend: QueryReferenceWorker) -> None: + with client() as c: + no_offer = attach(c, replace(CAPS, query_pushdown=None)) + selected = attach(c) + assert no_offer.query_pushdown is None + assert present(selected.query_pushdown).semantic_profiles == [QUERY_EXPLICIT] + assert selected.attach_opaque_data_required + with pytest.raises(Exception, match="query|capability"): + c.catalog_query_prepare(request(no_offer)) + other = QueryReferenceWorker(quiet=True) + other._signing_key = KEY + plain = other._open_attach_context(selected.attach_opaque_data, None) + assert plain is not None + assert attach_capabilities.agreement(plain) == CatalogCapabilities() + offer, agreement = present(attach_capabilities.query_agreement(plain)) + assert offer == OFFER and agreement == selected.query_pushdown + assert other._unwrap_attach_full(selected.attach_opaque_data) == attach_capabilities.split(plain)[1] + for payload in [b"VGI\0CAPQ\0", b"VGI\0CAPQ\0\0\0\0\1x", plain[:25]]: + with pytest.raises(ValueError): + attach_capabilities.agreement(payload) + with client() as c: + result = c.catalog_query_prepare(request(selected)) + assert result.status == "accepted" + assert counts()["starts"] == 0 + + +@pytest.mark.parametrize("name,query", [("value", "SELECT $value AS result"), ("1", "SELECT $1 AS result")]) +def test_prepare_bind_do_not_execute_and_values_rebind_without_casts( + backend: QueryReferenceWorker, name: str, query: str +) -> None: + with client() as c: + attached = attach(c) + first = parameters(None, name) + prepared = c.catalog_query_prepare(request(attached, first, query)) + bound = backend.bind(bind_request(attached, prepared, first), CTX) + assert bound.output_schema == pa.schema([pa.field("result", pa.int32())]) + assert counts()["starts"] == 0 + for value in [None, 7, -2]: + batches = list( + c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, + prepared=prepared, + parameters=parameters(value, name), + ) + ) + assert pa.Table.from_batches(batches).to_pydict() == {"result": [value]} + assert counts() == {"preparations": 1, "executions": 0, "fragments": 0, "starts": 3} + with pytest.raises(Exception, match="schema"): + list( + c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, + prepared=prepared, + parameters=parameters(7, name, pa.int64()), + ) + ) + assert counts()["starts"] == 3 + + +def test_repeated_placeholder_empty_result_and_ordered_multibatch(backend: QueryReferenceWorker) -> None: + with client() as c: + attached = attach(c) + for query, values, expected in [ + ("SELECT $value + $value AS result", parameters(3), [6]), + ( + "SELECT i * $factor AS result FROM numbers ORDER BY i", + parameters(5, "factor"), + [i * 5 for i in range(7)], + ), + ("SELECT i AS result FROM numbers WHERE false", QueryParameters.empty(), []), + ]: + prepared = c.catalog_query_prepare(request(attached, values, query)) + batches = list( + c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, prepared=prepared, parameters=values + ) + ) + assert [v for b in batches for v in b.column(0).to_pylist()] == expected + assert counts()["executions"] == counts()["fragments"] == 0 + declined = c.catalog_query_prepare(request(attached, query="DELETE FROM numbers")) + assert declined.status == "declined" and declined.reason_code == "unsupported_query" + assert counts()["starts"] == 3 + + +def test_receipts_bind_attachment_transaction_revision_and_release( + backend: QueryReferenceWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + with client() as c: + attached, other = attach(c), attach(c) + tx = present(c.catalog_transaction_begin(attach_opaque_data=attached.attach_opaque_data)) + prepared = c.catalog_query_prepare(request(attached, tx=tx)) + for wrong_attach, wrong_tx in [(other, tx), (attached, None)]: + with pytest.raises(Exception, match="opaque|query|transaction"): + backend.bind(bind_request(wrong_attach, prepared, tx=wrong_tx), CTX) + monkeypatch.setattr(QueryReferenceProvider, "revision", "changed") + with pytest.raises(Exception, match="reattach"): + backend.bind(bind_request(attached, prepared, tx=tx), CTX) + c.catalog_query_release( + attach_opaque_data=attached.attach_opaque_data, + prepared_query_token=present(prepared.prepared_query_token), + transaction_opaque_data=tx, + ) + c.catalog_query_release( + attach_opaque_data=attached.attach_opaque_data, + prepared_query_token=present(prepared.prepared_query_token), + transaction_opaque_data=tx, + ) + assert counts()["preparations"] == counts()["starts"] == 0 + monkeypatch.setattr(QueryReferenceProvider, "revision", "finite-reference-1") + with pytest.raises(Exception, match="missing"): + backend.bind(bind_request(attached, prepared, tx=tx), CTX) + + +@pytest.mark.parametrize( + "change", + [ + {"projection_ids": [0, 0]}, + {"projection_ids": []}, + {"row_limit": 1}, + {"split_tokens": []}, + {"execution_id": b"secondary", "init_opaque_data": b"token"}, + {"tablesample_percentage": 10.0}, + ], +) +def test_query_executor_rejects_alternate_init_before_start( + backend: QueryReferenceWorker, change: dict[str, Any] +) -> None: + with client() as c: + attached = attach(c) + prepared = c.catalog_query_prepare(request(attached)) + call = bind_request(attached, prepared) + bound = backend.bind(call, CTX) + init = InitRequest(bind_call=call, output_schema=bound.output_schema, **change) + with pytest.raises(ValueError): + backend.init(init, CTX) + assert counts()["starts"] == 0 + + +def test_generator_close_transaction_end_and_detach_cleanup(backend: QueryReferenceWorker) -> None: + with client() as c: + attached = attach(c) + values = QueryParameters.empty() + query = "SELECT i AS result FROM numbers ORDER BY i" + prepared = c.catalog_query_prepare(request(attached, values, query)) + rows = c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, prepared=prepared, parameters=values + ) + assert next(rows).column(0).to_pylist() == [0, 1] + rows.close() + assert counts()["executions"] == counts()["fragments"] == 0 + tx = present(c.catalog_transaction_begin(attach_opaque_data=attached.attach_opaque_data)) + prepared = c.catalog_query_prepare(request(attached, values, query, tx)) + c.catalog_transaction_rollback(attach_opaque_data=attached.attach_opaque_data, transaction_opaque_data=tx) + with pytest.raises(Exception, match="transaction|missing"): + list( + c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, + prepared=prepared, + parameters=values, + transaction_opaque_data=tx, + ) + ) + c.catalog_detach(attach_opaque_data=attached.attach_opaque_data) + assert counts()["preparations"] == 0 + + +def test_meta_worker_routes_prepare_and_release_with_private_agreement(backend: QueryReferenceWorker) -> None: + meta = MetaWorker([backend]) + meta._signing_key = KEY + attached = meta.catalog_attach( + CatalogAttachRequest( + name="queries", data_version_spec=None, implementation_version=None, client_capabilities=CAPS + ) + ) + prepared = meta.catalog_query_prepare(request(attached)) + assert meta.bind(bind_request(attached, prepared), CTX).output_schema.names == ["result"] + cast(VgiProtocol, meta).catalog_query_release( + attach_opaque_data=attached.attach_opaque_data, prepared_query_token=present(prepared.prepared_query_token) + ) + assert counts()["preparations"] == 0 + + +def test_quota_and_expiry_are_bounded_and_cleanup_needs_no_client_release( + backend: QueryReferenceWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + with client() as c: + attached = attach(c) + monkeypatch.setattr(QueryReferenceProvider, "max_resources", 1) + c.catalog_query_prepare(request(attached)) + with pytest.raises(Exception, match="quota"): + c.catalog_query_prepare(request(attached)) + with closing(sqlite3.connect(QueryReferenceCatalog.database_path)) as db, db: + db.execute("UPDATE query_preparations SET expires=0") + provider = backend._get_catalog().query_provider + assert isinstance(provider, QueryReferenceProvider) + provider.sweep() + assert counts()["preparations"] == 0 + assert c.catalog_query_prepare(request(attached)).status == "accepted" + + +def test_fresh_worker_resumes_ordered_fragments_after_preparation_release(backend: QueryReferenceWorker) -> None: + with client() as c: + attached = attach(c) + values = QueryParameters.empty() + prepared = c.catalog_query_prepare(request(attached, values, "SELECT i AS result FROM numbers ORDER BY i")) + with client(attached.attach_opaque_data) as c: + call = bind_request(attached, prepared, values) + cursor = c.table_scan_resumable(function_name=QUERY_EXECUTOR, schema_path=[], arguments=call.arguments) + first, token = cursor.next() + assert first is not None + assert first.column(0).to_pylist() == [0, 1] and token is not None + cursor.close() # HTTP releases the local reader; the execution has its own lease. + c.catalog_query_release( + attach_opaque_data=attached.attach_opaque_data, prepared_query_token=present(prepared.prepared_query_token) + ) + assert counts()["preparations"] == 0 and counts()["starts"] == 1 + # A different app/Worker and connection recover only the shared fragments. + with client() as c: + with pytest.raises(ValueError, match="table_scan_continue"): + c.table_scan_resumable(function_name=QUERY_EXECUTOR, schema_path=[], resume_token=token) + cursor = c.table_scan_continue(resume_token=token) + rows = [0, 1] + while True: + batch, _ = cursor.next() + if batch is None: + break + rows.extend(int(present(value)) for value in batch.column(0).to_pylist()) + cursor.close() + assert rows == list(range(7)) + assert counts() == {"preparations": 0, "executions": 0, "fragments": 0, "starts": 1} + + +@pytest.mark.parametrize("failure", ["missing", "wrong_schema", "expired", "policy"]) +def test_failed_continuation_never_reexecutes_or_falls_back( + backend: QueryReferenceWorker, monkeypatch: pytest.MonkeyPatch, failure: str +) -> None: + with client() as c: + attached = attach(c) + values = QueryParameters.empty() + prepared = c.catalog_query_prepare(request(attached, values, "SELECT i AS result FROM numbers ORDER BY i")) + with client(attached.attach_opaque_data) as c: + cursor = c.table_scan_resumable( + function_name=QUERY_EXECUTOR, schema_path=[], arguments=bind_request(attached, prepared, values).arguments + ) + _, token = cursor.next() + cursor.close() + assert token is not None + if failure == "policy": + monkeypatch.setattr(QueryReferenceProvider, "revision", "changed") + else: + with closing(sqlite3.connect(QueryReferenceCatalog.database_path)) as db, db: + if failure == "missing": + db.execute("DELETE FROM query_executions") + elif failure == "expired": + db.execute("UPDATE query_executions SET expires=0") + else: + from vgi_rpc.utils import serialize_record_batch_bytes + + wrong = serialize_record_batch_bytes(pa.record_batch([pa.array(["wrong"])], names=["result"])) + db.execute("UPDATE query_fragments SET payload=? WHERE position>0", (wrong,)) + with client() as c: + cursor = c.table_scan_continue(resume_token=token) + with pytest.raises(Exception, match="query|schema|reattach"): + cursor.next() + cursor.close() + assert counts()["starts"] == 1 + assert counts()["executions"] == counts()["fragments"] == 0 + + +def test_query_receipts_are_authentication_bound_and_tampering_rejects( + backend: QueryReferenceWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + import vgi.query_execution as execution + + alice = AuthContext(domain="fixture", authenticated=True, principal="alice") + bob = replace(alice, principal="bob") + for module in ["vgi.worker", "vgi.query_execution"]: + monkeypatch.setattr(f"{module}.current_auth", lambda: alice) + attached = backend.catalog_attach( + CatalogAttachRequest( + name="queries", data_version_spec=None, implementation_version=None, client_capabilities=CAPS + ) + ) + prepared = backend.catalog_query_prepare(request(attached)) + assert prepared.prepared_query_token is not None + assert attached.query_pushdown is not None + tampered = replace( + prepared, + prepared_query_token=present(prepared.prepared_query_token)[:-1] + + bytes([prepared.prepared_query_token[-1] ^ 1]), + ) + with pytest.raises(Exception, match="opaque|query"): + backend.bind(bind_request(attached, tampered), CTX) + # Even if the enclosing attachment could be opened, a receipt cannot be + # reinterpreted under a different principal's query resource owner. + owner = execution._owner(backend, attached.attach_opaque_data, None) + other_scope = execution.owner_from_plaintext(b"0" * 16 + owner.attach_opaque_data) + with pytest.raises(Exception, match="opaque|query"): + execution._open(backend, prepared.prepared_query_token, other_scope, attached.query_pushdown, "prepare") + for module in ["vgi.worker", "vgi.query_execution"]: + monkeypatch.setattr(f"{module}.current_auth", lambda: bob) + with pytest.raises(Exception, match="opaque|query|attach"): + backend.bind(bind_request(attached, prepared), CTX) + assert counts()["starts"] == 0 + + +def test_reserved_executor_is_hidden_and_cannot_be_registered(backend: QueryReferenceWorker) -> None: + from vgi.query_execution import executor_type + from vgi.worker import Worker + + internal = executor_type(backend) + assert internal is executor_type(backend) + assert QUERY_EXECUTOR not in type(backend)._build_registry() + + class InvalidWorker(Worker): + functions = [internal] + + with pytest.raises(ValueError, match="reserved"): + InvalidWorker._build_registry() diff --git a/tests/catalog/test_required_filters.py b/tests/catalog/test_required_filters.py index 337bc78d..9a64b3ac 100644 --- a/tests/catalog/test_required_filters.py +++ b/tests/catalog/test_required_filters.py @@ -1,91 +1,59 @@ -"""Unit tests for the ``Table.required_filters`` field. - -The field is purely declarative on the Python side — enforcement runs in the C++ -optimizer extension. It is conjunctive normal form: an AND (outer tuple) of -OR-groups (inner tuples) of dotted-path column references. These tests cover: - -1. Default-empty behaviour (no field set → ``TableInfo`` ships an empty list). -2. Populated round-trip (singleton groups + a multi-path OR-group preserved). -3. Validation: each path's leading dotted segment must be a real column. -4. Empty-string paths and empty OR-groups are rejected loudly. -""" +"""Required scan paths preserve literal identifiers through table metadata IPC.""" from __future__ import annotations import pyarrow as pa import pytest +from vgi_rpc.utils import deserialize_record_batch +from vgi.catalog import TableInfo from vgi.catalog.descriptors import Table - -class TestRequiredFiltersField: - """Behaviour of the declarative required_filters field.""" - - def _bbox_columns(self) -> pa.Schema: - """Return a small schema with a ``bbox`` STRUCT plus flat id/ticker.""" - fields: list[tuple[str, pa.DataType]] = [ - ("id", pa.int64()), - ("ticker", pa.string()), - ( - "bbox", - pa.struct( - [ - ("xmin", pa.float64()), - ("ymin", pa.float64()), - ("xmax", pa.float64()), - ("ymax", pa.float64()), - ] - ), - ), - ] - return pa.schema(fields) - - def test_default_is_empty_tuple(self) -> None: - """Default is an empty tuple; TableInfo wire field is an empty list.""" - t = Table(name="t", columns=self._bbox_columns()) - assert t.required_filters == () - info = t.to_table_info(["main"]) - assert info.required_filters == [] - - def test_singleton_groups_round_trip(self) -> None: - """Singleton groups (the AND-only case) survive as a list of lists.""" - groups = (("bbox.xmin",), ("bbox.xmax",), ("bbox.ymin",), ("bbox.ymax",)) - t = Table(name="place", columns=self._bbox_columns(), required_filters=groups) - assert t.required_filters == groups - info = t.to_table_info(["main"]) - assert info.required_filters == [list(g) for g in groups] - - def test_or_group_round_trip(self) -> None: - """A multi-path OR-group survives alongside a singleton mandatory group.""" - groups = (("id",), ("id", "ticker")) - t = Table(name="t", columns=self._bbox_columns(), required_filters=groups) - info = t.to_table_info(["main"]) - assert info.required_filters == [["id"], ["id", "ticker"]] - - def test_top_level_path(self) -> None: - """A top-level column name (no dots) is a valid path.""" - t = Table(name="t", columns=self._bbox_columns(), required_filters=(("id",),)) - assert t.to_table_info(["main"]).required_filters == [["id"]] - - def test_nested_path_is_not_unpacked_for_validation(self) -> None: - """Only the leading dotted segment is validated; deeper segments pass through.""" - # Subfield validity isn't checked here (descriptor doesn't unpack struct - # schemas) — only the leading segment matters. Typos like ``bbox.nope`` - # go through and DuckDB catches them at scan time. - t = Table(name="t", columns=self._bbox_columns(), required_filters=(("bbox.nope",),)) - assert t.required_filters == (("bbox.nope",),) - - def test_unknown_leading_segment_raises(self) -> None: - """A path whose leading segment is not a real column is rejected.""" - with pytest.raises(ValueError, match=r"unknown column 'nope'"): - Table(name="t", columns=self._bbox_columns(), required_filters=(("nope.xmin",),)) - - def test_empty_string_path_raises(self) -> None: - """An empty string path is rejected loudly rather than silently accepted.""" - with pytest.raises(ValueError, match="must not contain empty strings"): - Table(name="t", columns=self._bbox_columns(), required_filters=(("",),)) - - def test_empty_group_raises(self) -> None: - """An empty OR-group is rejected loudly.""" - with pytest.raises(ValueError, match="must not contain empty groups"): - Table(name="t", columns=self._bbox_columns(), required_filters=((),)) +COLUMNS = pa.schema( + [ + ("a.b", pa.int64()), + ("a", pa.struct([("b", pa.int64()), ('quoted."field', pa.int64())])), + ("id", pa.int64()), + ] +) + + +def test_required_filters_wire_round_trip() -> None: + """AND/OR grouping and each raw component survive the real TableInfo codec.""" + groups = ((("a.b",), ("a", "b")), (("a", 'quoted."field'),)) + info = Table(name="t", columns=COLUMNS, required_filters=groups).to_table_info(["main"]) + batch, _ = deserialize_record_batch(info.serialize_to_bytes()) + assert batch.schema.field("required_filters").type == pa.list_(pa.list_(pa.list_(pa.string()))) + assert TableInfo.deserialize_from_batch(batch).required_filters == [[["a.b"], ["a", "b"]], [["a", 'quoted."field']]] + + +def test_empty_requirements() -> None: + """No requirements is an explicit empty outer list.""" + table = Table(name="t", columns=COLUMNS) + assert table.required_filters == () + assert table.to_table_info(["main"]).required_filters == [] + + +@pytest.mark.parametrize( + "groups", + [ + ((),), + (((),),), + ((("",),),), + ((("a\0",),),), + ((("missing",),),), + ((("a", "missing"),),), + ((("id", "child"),),), + ((("a.b",), ("a.b",)),), + ], +) +def test_malformed_or_unresolved_paths(groups: tuple[tuple[tuple[str, ...], ...], ...]) -> None: + """Reject empty, duplicate, NUL, missing and non-struct paths before publication.""" + with pytest.raises(ValueError): + Table(name="t", columns=COLUMNS, required_filters=groups) + + +def test_legacy_strings_are_not_a_decoder_fallback() -> None: + """Old dotted strings need an explicit migration decision.""" + with pytest.raises(TypeError, match="raw identifier components"): + Table(name="t", columns=COLUMNS, required_filters=(("a.b",),)) # type: ignore[arg-type] diff --git a/tests/catalog/test_schema_metadata_protocol.py b/tests/catalog/test_schema_metadata_protocol.py new file mode 100644 index 00000000..dc2d7c3b --- /dev/null +++ b/tests/catalog/test_schema_metadata_protocol.py @@ -0,0 +1,218 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103, F811 +"""Schema creation metadata shares durable snapshots with typed options.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import cast + +import pyarrow as pa +import pytest +from vgi_rpc.utils import serialize_record_batch_bytes + +from vgi.catalog import CatalogMetadataChanges, OnConflict +from vgi.catalog.capabilities import SCHEMA_METADATA_ALTER, SCHEMA_OPTIONS_ALTER, SCHEMA_OPTIONS_CREATE, TRANSACTION_DDL +from vgi.catalog.options import OptionChanges +from vgi.exceptions import CatalogCapabilityError, OpaqueDataNotRecognizedError +from vgi.meta_worker import MetaWorker +from vgi.protocol import VgiProtocol + +from .schema_options_reference import PROFILES, SchemaOptionsCatalog, SchemaOptionsWorker, values +from .test_schema_options_protocol import OFFER, _attach, _client, _request, backend # noqa: F401 +from .typing_helpers import CatalogSchemaArgs, present + +pytestmark = pytest.mark.usefixtures("backend") + + +def test_metadata_options_and_conflicts_survive_transaction_reconnect() -> None: + with _client() as client: + token = _attach(client).attach_opaque_data + target: CatalogSchemaArgs = dict(attach_opaque_data=token, path=["a.b"]) + client.schema_create(**target, comment="", tags={"owner": "one"}, options=values(limit=25)) + original = client.schema_get(**target) + version = client.catalog_version(attach_opaque_data=token) + client.schema_create(**target, on_conflict=OnConflict.IGNORE, comment="ignored", options=values(limit=3)) + assert client.catalog_version(attach_opaque_data=token) == version + ignored = client.schema_get(**target) + assert present(ignored).comment == "" and present(ignored).tags == {"owner": "one"} + assert present(present(ignored).options).values.batch.equals(present(present(original).options).values.batch) + tx = client.catalog_transaction_begin(attach_opaque_data=token) + client.schema_create(**target, on_conflict=OnConflict.REPLACE, transaction_opaque_data=tx) + pending = client.schema_get(**target, transaction_opaque_data=tx) + assert ( + present(pending).comment is None + and present(pending).tags == {} + and present(present(pending).options).explicit_names == [] + ) + assert present(client.schema_get(**target)).comment == "" + with _client() as client: + client.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + restored = client.schema_get(**target) + assert present(restored).comment == "" and present(restored).tags == {"owner": "one"} + assert present(present(restored).options).values.batch.equals(present(present(original).options).values.batch) + assert client.catalog_version(attach_opaque_data=token) == version + tx = client.catalog_transaction_begin(attach_opaque_data=token) + client.schema_alter_options( + **target, transaction_opaque_data=tx, changes=OptionChanges(set_values=values(limit=30)) + ) + with _client() as client: + client.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + updated = client.schema_get(**target) + assert present(updated).comment == "" and present(updated).tags == {"owner": "one"} + assert present(present(updated).options).values.batch.column("limit")[0].as_py() == 30 + tx = client.catalog_transaction_begin(attach_opaque_data=token) + client.schema_create( + **target, + on_conflict=OnConflict.REPLACE, + comment="committed", + tags={"owner": "two"}, + transaction_opaque_data=tx, + ) + with _client() as client: + client.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + replaced = client.schema_get(**target) + assert present(replaced).comment == "committed" and present(replaced).tags == {"owner": "two"} + assert present(present(replaced).options).explicit_names == [] + + +def test_atomic_patch_noops_versions_and_snapshot_invalidation() -> None: + with _client() as client: + token = _attach(client).attach_opaque_data + target: CatalogSchemaArgs = dict(attach_opaque_data=token, path=["a.b"]) + client.schema_create( + **target, comment="before", tags={"keep": "yes", "old": "remove"}, options=values(limit=25) + ) + original = client.schema_get(**target) + snapshot = client.contents(attach_opaque_data=token) + client.schema_alter_metadata( + **target, + changes=CatalogMetadataChanges(set_comment=True, comment="", set_tags={"new": ""}, remove_tags=["old"]), + ) + changed = client.schema_get(**target) + assert present(changed).comment == "" and present(changed).tags == {"keep": "yes", "new": ""} + assert present(present(changed).options).values.batch.equals( + present(present(original).options).values.batch, check_metadata=True + ) + version = client.catalog_version(attach_opaque_data=token) + assert version > snapshot.catalog_version + refreshed = client.contents(attach_opaque_data=token, if_none_match=snapshot.etag) + assert not refreshed.not_modified and refreshed.etag != snapshot.etag + assert ( + present(next(s.schema for s in refreshed.schemas if s.schema.path == ["a.b"])).tags == present(changed).tags + ) + for patch in [ + CatalogMetadataChanges(), + CatalogMetadataChanges(remove_tags=["absent"]), + CatalogMetadataChanges(set_comment=True, comment="", set_tags={"new": ""}), + ]: + client.schema_alter_metadata(**target, changes=patch) + assert client.catalog_version(attach_opaque_data=token) == version + assert client.contents(attach_opaque_data=token, if_none_match=refreshed.etag).not_modified + # Nested and literal dotted schemas must remain separate mutation targets. + client.schema_create(attach_opaque_data=token, path=["a"]) + client.schema_create(attach_opaque_data=token, path=["a", "b"], comment="nested") + client.schema_alter_metadata(**target, changes=CatalogMetadataChanges(set_comment=True)) + assert present(client.schema_get(**target)).comment is None + assert present(client.schema_get(attach_opaque_data=token, path=["a", "b"])).comment == "nested" + + +def test_patch_transaction_isolation_rollback_commit_and_reconnect() -> None: + with _client() as client: + token = _attach(client).attach_opaque_data + target: CatalogSchemaArgs = dict(attach_opaque_data=token, path=["main"]) + snapshot = client.contents(attach_opaque_data=token) + tx = client.catalog_transaction_begin(attach_opaque_data=token) + client.schema_alter_metadata( + **target, + transaction_opaque_data=tx, + changes=CatalogMetadataChanges(set_comment=True, comment="pending", set_tags={"a": "one"}), + ) + pending = client.schema_get(**target, transaction_opaque_data=tx) + assert present(pending).comment == "pending" and present(pending).tags == {"a": "one"} + assert present(client.schema_get(**target)).comment is None + assert client.contents(attach_opaque_data=token, if_none_match=snapshot.etag).not_modified + with _client() as client: + client.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert present(client.schema_get(**target)).comment is None + assert client.contents(attach_opaque_data=token, if_none_match=snapshot.etag).not_modified + tx = client.catalog_transaction_begin(attach_opaque_data=token) + client.schema_alter_metadata( + **target, + transaction_opaque_data=tx, + changes=CatalogMetadataChanges(set_comment=True, comment="committed", set_tags={"a": "two"}), + ) + with _client() as client: + client.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + committed = client.schema_get(**target) + assert present(committed).comment == "committed" and present(committed).tags == {"a": "two"} + assert not client.contents(attach_opaque_data=token, if_none_match=snapshot.etag).not_modified + + +@pytest.mark.parametrize("meta", [False, True]) +def test_patch_requires_current_negotiation_and_routes_sealed_handles( + backend: SchemaOptionsWorker, monkeypatch: pytest.MonkeyPatch, meta: bool +) -> None: + # MetaWorker installs protocol forwarding methods dynamically. + worker = cast(VgiProtocol, MetaWorker([backend]) if meta else backend) + legacy = worker.catalog_attach(_request(None)).attach_opaque_data + patch = CatalogMetadataChanges(set_tags={"a": "new"}).serialize_to_bytes() + with pytest.raises(CatalogCapabilityError, match=SCHEMA_METADATA_ALTER): + worker.catalog_schema_alter_metadata(attach_opaque_data=legacy, path=["main"], changes=patch) + token = worker.catalog_attach(_request()).attach_opaque_data + worker.catalog_schema_alter_metadata(attach_opaque_data=token, path=["main"], changes=patch) + assert worker.catalog_schema_get(attach_opaque_data=token, path=["main"]).to_optional().tags == {"a": "new"} + before = worker.catalog_version(attach_opaque_data=token).version + monkeypatch.setattr( + SchemaOptionsCatalog, "profiles", replace(PROFILES, ddl=[SCHEMA_OPTIONS_ALTER, SCHEMA_OPTIONS_CREATE]) + ) + with pytest.raises(CatalogCapabilityError, match=SCHEMA_METADATA_ALTER): + worker.catalog_schema_alter_metadata( + attach_opaque_data=token, + path=["main"], + changes=CatalogMetadataChanges(set_comment=True, comment="bad").serialize_to_bytes(), + ) + assert worker.catalog_version(attach_opaque_data=token).version == before + assert worker.catalog_schema_get(attach_opaque_data=token, path=["main"]).to_optional().comment is None + + +def test_patch_rejects_without_transaction_ddl_and_foreign_transaction(backend: SchemaOptionsWorker) -> None: + token = backend.catalog_attach( + _request(replace(OFFER, catalog=replace(PROFILES, transaction=[]))) + ).attach_opaque_data + tx = backend.catalog_transaction_begin(token).transaction_opaque_data + patch = CatalogMetadataChanges(set_tags={"a": "new"}).serialize_to_bytes() + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + backend.catalog_schema_alter_metadata(token, ["main"], patch, transaction_opaque_data=tx) + full = backend.catalog_attach(_request()).attach_opaque_data + with pytest.raises(OpaqueDataNotRecognizedError): + backend.catalog_schema_alter_metadata(full, ["main"], patch, transaction_opaque_data=tx) + backend.catalog_transaction_rollback(token, present(tx)) + + +def test_invalid_patch_rejects_before_provider_even_with_missing_target_ignore( + backend: SchemaOptionsWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + token = backend.catalog_attach(_request()).attach_opaque_data + calls = [] + monkeypatch.setattr(backend._get_catalog(), "schema_alter_metadata", lambda **kwargs: calls.append(kwargs)) + row = {"set_comment": True, "comment": "bad", "set_tags": [("a", "new")], "remove_tags": ["a"]} + batch = pa.RecordBatch.from_pylist([row], schema=CatalogMetadataChanges.ARROW_SCHEMA) + with pytest.raises(ValueError, match="SET and REMOVE"): + backend.catalog_schema_alter_metadata( + token, ["absent"], serialize_record_batch_bytes(batch), ignore_not_found=True + ) + assert not calls + + +def test_missing_target_and_malformed_path_preserve_versions(backend: SchemaOptionsWorker) -> None: + token = backend.catalog_attach(_request()).attach_opaque_data + patch = CatalogMetadataChanges().serialize_to_bytes() + version = backend.catalog_version(token).version + backend.catalog_schema_alter_metadata(token, ["absent"], patch, ignore_not_found=True) + with pytest.raises(ValueError, match="does not exist"): + backend.catalog_schema_alter_metadata(token, ["absent"], patch) + for path in ([], [""], ["a\0b"]): + with pytest.raises(ValueError): + backend.catalog_schema_alter_metadata(token, path, patch, ignore_not_found=True) + assert backend.catalog_version(token).version == version diff --git a/tests/catalog/test_schema_options_protocol.py b/tests/catalog/test_schema_options_protocol.py new file mode 100644 index 00000000..1ccdf075 --- /dev/null +++ b/tests/catalog/test_schema_options_protocol.py @@ -0,0 +1,301 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Real RPC, durable transactions and capability isolation for schema options.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path +from typing import Any, cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi import attach_capabilities +from vgi.catalog.capabilities import SCHEMA_OPTIONS_ALTER, SCHEMA_OPTIONS_READ, TRANSACTION_DDL, CatalogCapabilities +from vgi.catalog.options import OptionChanges, OptionDefinitions +from vgi.client import Client +from vgi.exceptions import CatalogCapabilityError, OpaqueDataNotRecognizedError +from vgi.meta_worker import MetaWorker +from vgi.protocol import CatalogAttachRequest, ClientCapabilities, VgiProtocol +from vgi.serve import create_app +from vgi.typed_values import TypedValueBatch + +from .schema_options_reference import DEFINITIONS, PROFILES, SchemaOptionsCatalog, SchemaOptionsWorker, values +from .typing_helpers import present + +KEY = b"catalog-options-reference-signing-key" +OFFER = ClientCapabilities( + engine="reference-client", native_formats=[], catalogs=[], can_stream=False, filter_encodings=[], catalog=PROFILES +) + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> SchemaOptionsWorker: + monkeypatch.setattr(SchemaOptionsCatalog, "database_path", str(tmp_path / "catalog.sqlite"), raising=False) + worker = SchemaOptionsWorker(quiet=True) + worker._signing_key = KEY + return worker + + +def _request(offer: ClientCapabilities | None = OFFER) -> CatalogAttachRequest: + return CatalogAttachRequest( + name="options", data_version_spec=None, implementation_version=None, client_capabilities=offer + ) + + +def _client() -> Client: + return Client.from_http( + "http://vgi.test", + httpx_client=_SyncTestClient(create_app(SchemaOptionsWorker, describe=False, signing_key=KEY)), + ) + + +def _attach(client: Client, offer: ClientCapabilities | None = OFFER) -> Any: + return client.catalog_attach( + name="options", data_version_spec=None, implementation_version=None, client_capabilities=offer + ) + + +def test_public_http_lifecycle_and_durable_transaction_reconnect(backend: SchemaOptionsWorker) -> None: + with _client() as client: + attach = _attach(client) + token = attach.attach_opaque_data + assert attach.selected_capabilities == PROFILES + assert attach.catalog_capabilities == PROFILES + definitions = client.schema_option_definitions(attach_opaque_data=token, path=["a.b"]) + assert definitions.schema.equals(DEFINITIONS.schema, check_metadata=True) + client.schema_create(attach_opaque_data=token, path=["a.b"], options=values(label=None)) + created = client.schema_get(attach_opaque_data=token, path=["a.b"]) + assert created is not None and created.options is not None + assert created.options.explicit_names == ["label"] + assert created.options.values.batch.column("label")[0].as_py() is None + tx = client.catalog_transaction_begin(attach_opaque_data=token) + assert tx is not None + client.schema_alter_options( + attach_opaque_data=token, + path=["a.b"], + transaction_opaque_data=tx, + changes=OptionChanges(set_values=values(limit=25), reset_names=["label"]), + ) + pending = client.schema_get(attach_opaque_data=token, path=["a.b"], transaction_opaque_data=tx) + assert present(present(pending).options).explicit_names == ["limit"] + assert present(present(pending).options).values.batch.column("label")[0].as_py() == "default" + assert present(present(client.schema_get(attach_opaque_data=token, path=["a.b"])).options).explicit_names == [ + "label" + ] + with _client() as client: + client.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=tx) + client.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=tx) + assert present(present(client.schema_get(attach_opaque_data=token, path=["a.b"])).options).explicit_names == [ + "label" + ] + tx = client.catalog_transaction_begin(attach_opaque_data=token) + client.schema_alter_options( + attach_opaque_data=token, + path=["a.b"], + transaction_opaque_data=tx, + changes=OptionChanges(set_values=values(limit=25), reset_names=["label"]), + ) + with _client() as client: + client.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + client.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + committed = client.schema_get(attach_opaque_data=token, path=["a.b"]) + assert present(present(committed).options).explicit_names == ["limit"] + assert present(present(committed).options).values.batch.column("limit")[0].as_py() == 25 + assert present(present(committed).options).values.schema.equals(DEFINITIONS.schema, check_metadata=True) + + +@pytest.mark.parametrize("offer", [None, replace(OFFER, catalog=None), replace(OFFER, catalog=CatalogCapabilities())]) +def test_missing_offer_rejects_new_operations_before_provider( + backend: SchemaOptionsWorker, offer: ClientCapabilities | None, monkeypatch: pytest.MonkeyPatch +) -> None: + attach = backend.catalog_attach(_request(offer)) + assert attach.selected_capabilities == CatalogCapabilities() + calls = [] + monkeypatch.setattr(backend._get_catalog(), "schema_alter_options", lambda **kw: calls.append(kw)) + with pytest.raises(CatalogCapabilityError, match="catalog_schema_alter_options"): + backend.catalog_schema_alter_options( + attach.attach_opaque_data, ["main"], OptionChanges(set_values=values(limit=2)).serialize_to_bytes() + ) + with pytest.raises(CatalogCapabilityError, match="catalog_schema_create"): + backend.catalog_schema_create(attach.attach_opaque_data, ["new"], options=values(limit=2).serialize_to_bytes()) + assert not calls + backend.catalog_schema_create(attach.attach_opaque_data, ["legacy"]) + assert all(info.options is None for info in backend.catalog_schemas(attach.attach_opaque_data).to_infos()) + + +def test_selection_is_intersection_with_dependencies_and_unknown_versions() -> None: + unknown = CatalogCapabilities(read=[SCHEMA_OPTIONS_READ, "vgi.schema.options.read.v2"], ddl=[SCHEMA_OPTIONS_ALTER]) + assert PROFILES.select(unknown) == CatalogCapabilities(read=[SCHEMA_OPTIONS_READ], ddl=[SCHEMA_OPTIONS_ALTER]) + assert PROFILES.select(CatalogCapabilities(ddl=[SCHEMA_OPTIONS_ALTER])) == CatalogCapabilities() + assert PROFILES.select(None) == CatalogCapabilities() + with pytest.raises(ValueError, match="selected"): + unknown.validate_selected() + + +def test_current_provider_downgrade_rejects_before_effects( + backend: SchemaOptionsWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + attach = backend.catalog_attach(_request()) + before = backend.catalog_schema_get(attach.attach_opaque_data, ["main"]).to_optional().options.serialize_to_bytes() + monkeypatch.setattr(SchemaOptionsCatalog, "profiles", CatalogCapabilities(read=[SCHEMA_OPTIONS_READ])) + with pytest.raises(CatalogCapabilityError): + backend.catalog_schema_alter_options( + attach.attach_opaque_data, ["main"], OptionChanges(set_values=values(limit=20)).serialize_to_bytes() + ) + assert ( + backend.catalog_schema_get(attach.attach_opaque_data, ["main"]).to_optional().options.serialize_to_bytes() + == before + ) + + +def test_transaction_requires_ddl_guarantee(backend: SchemaOptionsWorker) -> None: + attach = backend.catalog_attach(_request(replace(OFFER, catalog=replace(PROFILES, transaction=[])))) + tx = backend.catalog_transaction_begin(attach.attach_opaque_data).transaction_opaque_data + assert tx is not None + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + backend.catalog_schema_alter_options( + attach.attach_opaque_data, ["main"], OptionChanges().serialize_to_bytes(), tx + ) + backend.catalog_transaction_rollback(attach.attach_opaque_data, tx) + + +def test_metadata_and_etags_are_scoped_to_agreement_even_with_provider_cache(backend: SchemaOptionsWorker) -> None: + full = backend.catalog_attach(_request()) + legacy = backend.catalog_attach(_request(None)) + full_contents = backend.catalog_contents(full.attach_opaque_data) + legacy_contents = backend.catalog_contents(legacy.attach_opaque_data, if_none_match=full_contents.etag) + assert not legacy_contents.not_modified + assert full_contents.etag != legacy_contents.etag + from vgi.catalog import SchemaInfo + from vgi.catalog.contents_codec import decode_collection + + assert decode_collection(SchemaInfo, full_contents.schemas)[0].options is not None + assert decode_collection(SchemaInfo, legacy_contents.schemas)[0].options is None + assert backend.catalog_contents(full.attach_opaque_data, if_none_match=full_contents.etag).not_modified + assert backend.catalog_contents(legacy.attach_opaque_data, if_none_match=legacy_contents.etag).not_modified + + +@pytest.mark.parametrize("meta", [False, True]) +def test_sealed_agreement_survives_routing_and_restart(backend: SchemaOptionsWorker, meta: bool) -> None: + worker = MetaWorker([backend]) if meta else backend + attach = worker.catalog_attach(_request()) + fresh = SchemaOptionsWorker(quiet=True) + fresh._signing_key = KEY + # MetaWorker installs protocol forwarding methods dynamically. + restarted = cast(VgiProtocol, MetaWorker([fresh]) if meta else fresh) + assert fresh._catalog_agreement(attach.attach_opaque_data) == PROFILES + assert len(present(fresh._unwrap_attach_full(attach.attach_opaque_data))) == 32 + assert len(present(fresh._unwrap_attach(attach.attach_opaque_data))) == 16 + restarted.catalog_schema_alter_options( + attach_opaque_data=attach.attach_opaque_data, + path=["main"], + changes=OptionChanges(set_values=values(limit=33)).serialize_to_bytes(), + ) + assert ( + restarted.catalog_schema_get(attach_opaque_data=attach.attach_opaque_data, path=["main"]) + .to_optional() + .options.values.batch.column("limit")[0] + .as_py() + == 33 + ) + + +def test_tampered_or_malformed_agreement_rejects(backend: SchemaOptionsWorker) -> None: + attach = backend.catalog_attach(_request()).attach_opaque_data + tampered = attach[:-1] + bytes([attach[-1] ^ 1]) + malformed = backend._seal_attach(b"VGI\x00CAP\x00\x00\x00\x00\xffshort") + for token in (tampered, malformed): + with pytest.raises(OpaqueDataNotRecognizedError): + backend.catalog_schema_alter_options(token, ["main"], OptionChanges().serialize_to_bytes()) + legacy = backend._seal_attach(b"x" * 32) + assert backend._catalog_agreement(legacy) == CatalogCapabilities() + assert attach_capabilities.split(b"x" * 32) == (None, b"x" * 32) + + +def test_invalid_mixed_change_has_no_effect_or_version_bump(backend: SchemaOptionsWorker) -> None: + token = backend.catalog_attach(_request()).attach_opaque_data + before = backend.catalog_version(token).version + with pytest.raises(ValueError, match="not mutable"): + backend.catalog_schema_alter_options( + token, ["main"], OptionChanges(set_values=values(limit=99), reset_names=["locked"]).serialize_to_bytes() + ) + after = backend.catalog_schema_get(token, ["main"]).to_optional() + assert after.options.values.batch.column("limit")[0].as_py() == 10 + assert backend.catalog_version(token).version == before + + +def test_conflicting_commits_do_not_overwrite_other_attachment(backend: SchemaOptionsWorker) -> None: + one = backend.catalog_attach(_request()).attach_opaque_data + two = backend.catalog_attach(_request()).attach_opaque_data + tx = backend.catalog_transaction_begin(one).transaction_opaque_data + backend.catalog_schema_alter_options( + one, ["main"], OptionChanges(set_values=values(limit=20)).serialize_to_bytes(), tx + ) + backend.catalog_schema_alter_options(two, ["main"], OptionChanges(set_values=values(limit=30)).serialize_to_bytes()) + with pytest.raises(ValueError, match="conflict"): + backend.catalog_transaction_commit(one, present(tx)) + assert backend.catalog_schema_get(two, ["main"]).to_optional().options.values.batch.column("limit")[0].as_py() == 30 + backend.catalog_transaction_rollback(one, present(tx)) + + +def test_required_nullable_option_and_reset_without_default() -> None: + fields: list[pa.Field[pa.DataType]] = [ + pa.field("required", pa.string()), + pa.field("defaulted", pa.int32(), nullable=False), + ] + schema = pa.schema(fields) + defs = OptionDefinitions( + schema=schema, + defaults=TypedValueBatch( + pa.record_batch([pa.array([1], type=pa.int32())], schema=pa.schema([schema.field("defaulted")])) + ), + create_names=["required"], + alter_names=["required", "defaulted"], + ) + with pytest.raises(ValueError, match="required options"): + defs.create() + required = TypedValueBatch( + pa.record_batch([pa.array([None], type=pa.string())], schema=pa.schema([schema.field("required")])) + ) + state = defs.create(required) + assert state.explicit_names == ["required"] + assert state.values.batch.column("required")[0].as_py() is None + with pytest.raises(ValueError, match="without defaults"): + defs.apply(state, OptionChanges(reset_names=["required"])) + restored = OptionDefinitions.deserialize_from_bytes(defs.serialize_to_bytes()) + assert restored.schema.equals(defs.schema, check_metadata=True) + assert restored.defaults.batch.equals(defs.defaults.batch, check_metadata=True) + + +def test_agreement_cache_remains_bound_to_authenticated_principal( + backend: SchemaOptionsWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + from vgi_rpc.rpc import AuthContext + + alice = AuthContext(domain="reference", authenticated=True, principal="alice") + bob = AuthContext(domain="reference", authenticated=True, principal="bob") + monkeypatch.setattr("vgi.worker.current_auth", lambda: alice) + token = backend.catalog_attach(_request()).attach_opaque_data + assert backend._catalog_agreement(token) == PROFILES + monkeypatch.setattr("vgi.worker.current_auth", lambda: bob) + with pytest.raises(OpaqueDataNotRecognizedError): + backend.catalog_schema_alter_options(token, ["main"], OptionChanges().serialize_to_bytes()) + monkeypatch.setattr("vgi.worker.current_auth", lambda: alice) + assert backend._catalog_agreement(token) == PROFILES + + +def test_transaction_cannot_move_to_another_negotiated_attachment(backend: SchemaOptionsWorker) -> None: + one = backend.catalog_attach(_request()).attach_opaque_data + two = backend.catalog_attach(_request()).attach_opaque_data + transaction = backend.catalog_transaction_begin(one).transaction_opaque_data + assert transaction is not None + with pytest.raises(OpaqueDataNotRecognizedError): + backend.catalog_schema_alter_options( + two, ["main"], OptionChanges(set_values=values(limit=45)).serialize_to_bytes(), transaction + ) + assert backend.catalog_schema_get(two, ["main"]).to_optional().options.values.batch.column("limit")[0].as_py() == 10 + backend.catalog_transaction_rollback(one, transaction) diff --git a/tests/catalog/test_serialization.py b/tests/catalog/test_serialization.py index 26409f1a..60862c72 100644 --- a/tests/catalog/test_serialization.py +++ b/tests/catalog/test_serialization.py @@ -2,7 +2,11 @@ """Tests for catalog dataclass serialization/deserialization.""" +from __future__ import annotations + +from collections.abc import Callable from decimal import Decimal +from typing import Any, cast import pyarrow as pa import pytest @@ -23,6 +27,32 @@ TableInfo, ViewInfo, ) +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.macros import MacroOverload, normalize_macro_overloads + +from .typing_helpers import present + + +def canonical_unique(owner: str, *paths: list[str]) -> list[bytes]: + """Explicit provider records for transport tests; no trust inferred from indices.""" + from vgi.catalog.constraints import ( + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintValidation, + ) + from vgi.catalog.dependencies import CatalogObjectRef + + return [ + ConstraintInfo( + ref=CatalogObjectRef(kind="constraint", schema_path=["main"], owner_table=owner, name=f"provider-key-{i}"), + definition=ConstraintDefinition(kind=ConstraintKind.UNIQUE, column_paths=[[p] for p in path]), + enforcement=ConstraintEnforcement.METADATA_ONLY, + validation=ConstraintValidation.UNVALIDATED, + ).serialize_to_bytes() + for i, path in enumerate(paths) + ] class TestCatalogAttachResultSerialization: @@ -203,9 +233,8 @@ def test_basic_round_trip(self) -> None: name="users", schema_path=["main"], columns=columns_bytes, - not_null_constraints=[0], - unique_constraints=[[0]], - check_constraints=["id > 0"], + constraints=canonical_unique("users", ["id"]), + definition_revision=b"opaque-revision", comment="Users table", tags={"category": "core"}, ) @@ -216,9 +245,8 @@ def test_basic_round_trip(self) -> None: assert restored.name == original.name assert restored.schema_path == original.schema_path assert restored.columns == original.columns - assert restored.not_null_constraints == original.not_null_constraints - assert restored.unique_constraints == original.unique_constraints - assert restored.check_constraints == original.check_constraints + assert restored.constraints == original.constraints + assert restored.definition_revision == original.definition_revision assert restored.comment == original.comment assert restored.tags == original.tags @@ -231,9 +259,7 @@ def test_empty_constraints(self) -> None: name="simple", schema_path=["main"], columns=columns_bytes, - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], comment=None, tags={}, ) @@ -241,9 +267,7 @@ def test_empty_constraints(self) -> None: batch, _ = deserialize_record_batch(serialized) restored = TableInfo.deserialize_from_batch(batch) - assert restored.not_null_constraints == [] - assert restored.unique_constraints == [] - assert restored.check_constraints == [] + assert restored.constraints == [] def test_multiple_unique_constraints(self) -> None: """Test with multiple unique constraints on multiple columns.""" @@ -254,9 +278,7 @@ def test_multiple_unique_constraints(self) -> None: name="multi", schema_path=["main"], columns=columns_bytes, - not_null_constraints=[0, 1], - unique_constraints=[[0], [1, 2]], - check_constraints=["a > 0", "b < 100"], + constraints=canonical_unique("multi", ["a"], ["b", "c"]), comment=None, tags={}, ) @@ -264,7 +286,7 @@ def test_multiple_unique_constraints(self) -> None: batch, _ = deserialize_record_batch(serialized) restored = TableInfo.deserialize_from_batch(batch) - assert restored.unique_constraints == [[0], [1, 2]] + assert [info.definition.column_paths for info in restored.constraint_infos()] == [[["a"]], [["b"], ["c"]]] def test_inlined_scan_function_round_trip(self) -> None: """``scan_function`` IPC bytes round-trip through TableInfo serialize/deserialize. @@ -284,9 +306,7 @@ def test_inlined_scan_function_round_trip(self) -> None: name="t", schema_path=["s"], columns=columns_bytes, - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], comment=None, tags={}, scan_function=sfr.serialize(), @@ -319,9 +339,7 @@ def test_inlined_cardinality_round_trip(self) -> None: name="t", schema_path=["s"], columns=columns_bytes, - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], comment=None, tags={}, cardinality_estimate=12345, @@ -342,9 +360,7 @@ def test_inlined_cardinality_partial(self) -> None: name="t", schema_path=["s"], columns=columns_bytes, - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], comment=None, tags={}, cardinality_estimate=estimate, @@ -367,9 +383,7 @@ def test_omitted_inline_fields_default_to_none(self) -> None: name="t", schema_path=["s"], columns=SerializedSchema(columns_schema.serialize().to_pybytes()), - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], comment=None, tags={}, ) @@ -403,9 +417,7 @@ def test_inlined_bind_result_round_trip(self) -> None: name="t", schema_path=["s"], columns=columns_bytes, - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], comment=None, tags={}, bind_result=bind_blob, @@ -447,9 +459,7 @@ def test_inlined_column_statistics_round_trip(self) -> None: name="t", schema_path=["s"], columns=columns_bytes, - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], comment=None, tags={}, supports_column_statistics=True, @@ -474,6 +484,7 @@ def test_basic_round_trip(self) -> None: name="user_summary", schema_path=["main"], definition="SELECT id, name FROM users", + aliases=["a.b", 'with"quote'], comment="Summary view", tags={"type": "summary"}, ) @@ -484,9 +495,16 @@ def test_basic_round_trip(self) -> None: assert restored.name == original.name assert restored.schema_path == original.schema_path assert restored.definition == original.definition + assert restored.aliases == original.aliases assert restored.comment == original.comment assert restored.tags == original.tags + @pytest.mark.parametrize("aliases", [[None], [""], ["bad\0name"], "a.b"]) + def test_invalid_aliases(self, aliases: Any) -> None: + """Malformed aliases must not disappear or change output positions.""" + with pytest.raises(ValueError, match="view aliases"): + ViewInfo(name="v", schema_path=["main"], definition="SELECT 1", comment=None, tags={}, aliases=aliases) + def test_complex_definition(self) -> None: """Test with complex SQL definition.""" original = ViewInfo( @@ -508,181 +526,105 @@ def test_complex_definition(self) -> None: restored = ViewInfo.deserialize_from_batch(batch) assert restored.definition == original.definition + assert restored.aliases == original.aliases class TestMacroInfoSerialization: - """Test MacroInfo serialization round-trip.""" + """The required complete family retains identity, values, types, and docs.""" - def test_scalar_macro_round_trip(self) -> None: - """Test round-trip with scalar macro and all fields.""" + @pytest.mark.parametrize("macro_type,definition", [(MacroType.SCALAR, "x + 1"), (MacroType.TABLE, "SELECT x")]) + def test_family_round_trip(self, macro_type: MacroType, definition: str) -> None: + """Required families preserve scalar and table definitions.""" original = MacroInfo( - name="multiply", - schema_path=["main"], - macro_type=MacroType.SCALAR, - parameters=["x", "y"], - definition="x * y", - comment="Multiply two values", - tags={"category": "math"}, + name="macro", + schema_path=["literal.schema"], + macro_type=macro_type, + overloads=normalize_macro_overloads( + macro_type=macro_type.value, + schema_path=["literal.schema"], + parameters=["x"], + definition=definition, + evaluation_context=QueryEvaluationContext(), + ), + comment="retained", + tags={"purpose": "test"}, ) - serialized = original.serialize_to_bytes() - batch, _ = deserialize_record_batch(serialized) - restored = MacroInfo.deserialize_from_batch(batch) - - assert restored.name == original.name - assert restored.schema_path == original.schema_path - assert restored.macro_type == MacroType.SCALAR - assert restored.parameters == ["x", "y"] - assert restored.definition == "x * y" - assert restored.comment == "Multiply two values" - assert restored.tags == {"category": "math"} - - def test_table_macro_round_trip(self) -> None: - """Test round-trip with table macro.""" - original = MacroInfo( - name="my_range", - schema_path=["main"], - macro_type=MacroType.TABLE, - parameters=["n"], - definition="SELECT * FROM range(n)", - comment=None, - tags={}, + restored = MacroInfo.deserialize_from_bytes(original.serialize_to_bytes()) + assert restored == original + assert restored.overloads[0].parameters[0].name == "x" + assert restored.overloads[0].definition == definition + assert not {"parameters", "definition", "arguments_schema", "parameter_default_values"}.intersection( + MacroInfo.ARROW_SCHEMA.names ) - serialized = original.serialize_to_bytes() - batch, _ = deserialize_record_batch(serialized) - restored = MacroInfo.deserialize_from_batch(batch) - - assert restored.macro_type == MacroType.TABLE - assert restored.parameters == ["n"] - assert restored.definition == "SELECT * FROM range(n)" + assert not MacroInfo.ARROW_SCHEMA.field("overloads").nullable - def test_parameter_default_values_round_trip(self) -> None: - """Test parameter_default_values RecordBatch survives round-trip with types.""" - defaults = pa.RecordBatch.from_pydict( - {"lo": pa.array([0], type=pa.int64()), "hi": pa.array([100], type=pa.int64())} - ) - original = MacroInfo( - name="clamp", - schema_path=["main"], - macro_type=MacroType.SCALAR, - parameters=["val", "lo", "hi"], - parameter_default_values=defaults, - definition="GREATEST(lo, LEAST(hi, val))", - comment=None, - tags={}, - ) - serialized = original.serialize_to_bytes() - batch, _ = deserialize_record_batch(serialized) - restored = MacroInfo.deserialize_from_batch(batch) + def test_typed_null_and_documentation_are_independent(self) -> None: + """Declared types, NULL defaults, and empty documentation survive independently.""" + from vgi.catalog.macros import MACRO_PROFILE, MacroOverload, MacroParameter + from vgi.typed_values import TypedValueBatch - assert restored.parameter_default_values is not None - assert restored.parameter_default_values.num_rows == 1 - assert restored.parameter_default_values.schema.names == ["lo", "hi"] - assert restored.parameter_default_values.column("lo").type == pa.int64() - assert restored.parameter_default_values.column("hi").type == pa.int64() - assert restored.parameter_default_values.column("lo")[0].as_py() == 0 - assert restored.parameter_default_values.column("hi")[0].as_py() == 100 - - def test_none_parameter_default_values(self) -> None: - """Test with None parameter_default_values.""" + field = pa.field("x", pa.int64(), metadata={b"type_note": b"retained"}) + declared = pa.schema([field], metadata={b"owner": b"schema"}) + literal = TypedValueBatch(pa.RecordBatch.from_arrays([pa.array([None], type=pa.int64())], schema=declared)) original = MacroInfo( - name="simple", + name="m", schema_path=["main"], macro_type=MacroType.SCALAR, - parameters=["x"], - parameter_default_values=None, - definition="x", comment=None, tags={}, + overloads=[ + MacroOverload( + parameters=[ + MacroParameter(name="x", declared_type=declared, default_value=literal, documentation="") + ], + definition="x", + profile=MACRO_PROFILE, + schema_path=["main"], + macro_type="scalar", + evaluation_context=QueryEvaluationContext(), + ) + ], ) - serialized = original.serialize_to_bytes() - batch, _ = deserialize_record_batch(serialized) - restored = MacroInfo.deserialize_from_batch(batch) - - assert restored.parameter_default_values is None + restored = MacroInfo.deserialize_from_bytes(original.serialize_to_bytes()).overloads[0].parameters[0] + assert present(restored.declared_type).equals(declared, check_metadata=True) + assert present(restored.default_value).batch.equals(present(literal).batch, check_metadata=True) + assert restored.has_default and restored.default_expression is None and restored.documentation == "" + + @pytest.mark.parametrize("family", [None, []]) + def test_missing_or_empty_family_rejects(self, family: list[MacroOverload] | None) -> None: + """A wire macro always contains at least one complete member.""" + with pytest.raises(ValueError, match="nonempty"): + MacroInfo( + name="m", + schema_path=["main"], + macro_type=MacroType.SCALAR, + overloads=cast(list[MacroOverload], family), + comment=None, + tags={}, + ) - def test_macro_type_enum_survival(self) -> None: - """MacroType enum survives serialization round-trip.""" - for macro_type in MacroType: - original = MacroInfo( - name="test", + def test_no_legacy_wire_constructor(self) -> None: + """Legacy single fields cannot construct the successor wire record.""" + with pytest.raises(TypeError, match="parameters"): + cast("Callable[..., MacroInfo]", MacroInfo)( + name="m", schema_path=["main"], - macro_type=macro_type, + macro_type=MacroType.SCALAR, parameters=[], definition="1", comment=None, tags={}, ) - serialized = original.serialize_to_bytes() - batch, _ = deserialize_record_batch(serialized) - restored = MacroInfo.deserialize_from_batch(batch) - assert restored.macro_type == macro_type - - def test_arguments_schema_round_trip(self) -> None: - """arguments_schema carries vgi_doc per documented param; absent doc -> no key.""" - from vgi.argument_spec import VGI_DOC_KEY, macro_arguments_schema, macro_parameter_docs_from_schema - - defaults = pa.RecordBatch.from_pydict({"lo": pa.array([0], type=pa.int64())}) - args = macro_arguments_schema( - parameters=["val", "lo", "hi"], - parameter_default_values=defaults, - parameter_docs={"val": "value to clamp", "hi": "upper bound"}, - ) - original = MacroInfo( - name="clamp", - schema_path=["main"], - macro_type=MacroType.SCALAR, - parameters=["val", "lo", "hi"], - parameter_default_values=defaults, - definition="GREATEST(lo, LEAST(hi, val))", - comment=None, - tags={}, - arguments_schema=args, - ) - serialized = original.serialize_to_bytes() - batch, _ = deserialize_record_batch(serialized) - restored = MacroInfo.deserialize_from_batch(batch) - - assert restored.arguments_schema is not None - rs = restored.arguments_schema - # One field per parameter, in order. - assert rs.names == ["val", "lo", "hi"] - # Field type tracks the default value type when known, else null. - assert rs.field("lo").type == pa.int64() - assert rs.field("val").type == pa.null() - assert rs.field("hi").type == pa.null() - # Documented params carry vgi_doc; undocumented (lo) has no key. - assert (rs.field("val").metadata or {}).get(VGI_DOC_KEY) == b"value to clamp" - assert (rs.field("hi").metadata or {}).get(VGI_DOC_KEY) == b"upper bound" - assert VGI_DOC_KEY not in (rs.field("lo").metadata or {}) - # Convenience extractor returns only documented params. - assert macro_parameter_docs_from_schema(rs) == {"val": "value to clamp", "hi": "upper bound"} - - def test_none_arguments_schema(self) -> None: - """arguments_schema defaults to None (older workers) and survives round-trip.""" - original = MacroInfo( - name="simple", - schema_path=["main"], - macro_type=MacroType.SCALAR, - parameters=["x"], - definition="x", - comment=None, - tags={}, - ) - serialized = original.serialize_to_bytes() - batch, _ = deserialize_record_batch(serialized) - restored = MacroInfo.deserialize_from_batch(batch) - assert restored.arguments_schema is None class TestMacroArgumentsSchemaWire: - """Macro per-parameter docs flow over create/list wire types.""" + """Local single-definition adapters publish canonical parameter records.""" - def test_declarative_macro_to_info_carries_docs(self) -> None: - """Declarative Macro.parameter_docs -> MacroInfo.arguments_schema vgi_doc.""" - from vgi.argument_spec import macro_parameter_docs_from_schema + def test_declarative_macro_to_info_carries_docs_and_literal_defaults(self) -> None: + """Local convenience declarations normalize into parameter records.""" from vgi.catalog.descriptors import Macro - m = Macro( + source = Macro( name="clamp", macro_type=MacroType.SCALAR, parameters=["x", "lo", "hi"], @@ -692,13 +634,15 @@ def test_declarative_macro_to_info_carries_docs(self) -> None: parameter_docs={"x": "value to clamp"}, definition="GREATEST(lo, LEAST(hi, x))", ) - info = m.to_macro_info(["main"]) - assert info.arguments_schema is not None - assert info.arguments_schema.names == ["x", "lo", "hi"] - assert macro_parameter_docs_from_schema(info.arguments_schema) == {"x": "value to clamp"} + parameters = source.to_macro_info(["main"]).overloads[0].parameters + assert [p.name for p in parameters] == ["x", "lo", "hi"] + assert parameters[0].documentation == "value to clamp" and parameters[0].default_value is None + assert present(parameters[1].default_value).batch.column(0)[0].as_py() == 0 + assert present(parameters[2].default_value).batch.column(0)[0].as_py() == 100 + assert all(p.declared_type is None for p in parameters) def test_declarative_macro_rejects_unknown_doc_param(self) -> None: - """parameter_docs keys must be in parameters (validated like defaults).""" + """Unknown documentation keys reject before normalization.""" from vgi.catalog.descriptors import Macro with pytest.raises(ValueError, match="documented parameter 'bogus' not found"): @@ -710,46 +654,36 @@ def test_declarative_macro_rejects_unknown_doc_param(self) -> None: definition="x", ) - def test_macro_create_request_round_trip(self) -> None: - """MacroCreateRequest carries arguments_schema over the wire.""" - from vgi.argument_spec import macro_arguments_schema, macro_parameter_docs_from_schema + def test_create_request_preserves_complete_family(self) -> None: + """CREATE uses only the mandatory family fields.""" from vgi.catalog import OnConflict from vgi.protocol import MacroCreateRequest - args = macro_arguments_schema( - parameters=["x", "y"], - parameter_docs={"x": "first", "y": "second"}, - ) - req = MacroCreateRequest( - attach_opaque_data=b"attach", + family = normalize_macro_overloads( + macro_type="scalar", schema_path=["main"], - name="add", - macro_type=MacroType.SCALAR, - parameters=["x", "y"], - definition="x + y", - on_conflict=OnConflict.ERROR, - arguments_schema=args, + parameters=["x"], + definition="x", + evaluation_context=QueryEvaluationContext(), ) - restored = MacroCreateRequest.deserialize_from_bytes(req.serialize_to_bytes()) - assert restored.arguments_schema is not None - assert macro_parameter_docs_from_schema(restored.arguments_schema) == {"x": "first", "y": "second"} - - def test_macro_create_request_none_arguments_schema(self) -> None: - """MacroCreateRequest.arguments_schema defaults to None and round-trips.""" - from vgi.catalog import OnConflict - from vgi.protocol import MacroCreateRequest - - req = MacroCreateRequest( + request = MacroCreateRequest( attach_opaque_data=b"attach", schema_path=["main"], - name="add", + name="m", macro_type=MacroType.SCALAR, - parameters=["x", "y"], - definition="x + y", + overloads=family, on_conflict=OnConflict.ERROR, ) - restored = MacroCreateRequest.deserialize_from_bytes(req.serialize_to_bytes()) - assert restored.arguments_schema is None + assert MacroCreateRequest.deserialize_from_bytes(request.serialize_to_bytes()) == request + assert MacroCreateRequest.ARROW_SCHEMA.names == [ + "attach_opaque_data", + "schema_path", + "name", + "macro_type", + "overloads", + "on_conflict", + "transaction_opaque_data", + ] class TestFunctionInfoSerialization: @@ -1170,7 +1104,7 @@ class TestArrowSchemaCorrectness: def test_catalog_attach_result_schema(self) -> None: """Verify CatalogAttachResult Arrow schema.""" schema = CatalogAttachResult.ARROW_SCHEMA - assert len(schema) == 18 + assert len(schema) == 21 assert schema.field("supports_catalog_contents").type == pa.bool_() assert schema.field("attach_opaque_data").type == pa.binary() assert schema.field("supports_transactions").type == pa.bool_() @@ -1190,7 +1124,7 @@ def test_catalog_attach_result_schema(self) -> None: def test_schema_info_schema(self) -> None: """Verify SchemaInfo Arrow schema.""" schema = SchemaInfo.ARROW_SCHEMA - assert len(schema) == 4 + assert len(schema) == 5 assert "attach_opaque_data" not in schema.names assert schema.field("path").type == pa.list_(pa.string()) assert schema.field("comment").type == pa.string() @@ -1204,18 +1138,19 @@ def test_table_info_schema(self) -> None: assert schema.field("name").type == pa.string() assert schema.field("schema_path").type == pa.list_(pa.string()) assert schema.field("columns").type == pa.binary() - assert schema.field("not_null_constraints").type == pa.list_(pa.int32()) - assert schema.field("unique_constraints").type == pa.list_(pa.list_(pa.int32())) - assert schema.field("check_constraints").type == pa.list_(pa.string()) + assert schema.field("constraints").type == pa.list_(pa.binary()) + assert not schema.field("constraints").nullable + assert schema.field("definition_revision").type == pa.binary() + assert schema.field("definition_revision").nullable # Inlined optional fields — populated to skip per-bind RPCs. assert schema.field("scan_function").type == pa.binary() assert schema.field("cardinality_estimate").type == pa.int64() assert schema.field("column_statistics").type == pa.binary() assert schema.field("bind_result").type == pa.binary() - assert schema.field("required_filters").type == pa.list_(pa.list_(pa.string())) - # required_filters is the LAST field (positional schema compatibility: - # old C++ extensions ignore-by-position trailing fields). - assert schema.field(len(schema) - 1).name == "required_filters" + assert schema.field("required_filters").type == pa.list_(pa.list_(pa.list_(pa.string()))) + assert schema.get_field_index("required_filters") == 17 + assert schema.field("options").type == pa.binary() and schema.field("options").nullable + assert schema.field("layout").type == pa.binary() and schema.field("layout").nullable def test_view_info_schema(self) -> None: """Verify ViewInfo Arrow schema.""" diff --git a/tests/catalog/test_statement_contract.py b/tests/catalog/test_statement_contract.py new file mode 100644 index 00000000..e900b118 --- /dev/null +++ b/tests/catalog/test_statement_contract.py @@ -0,0 +1,141 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""The explicit mutation profile uses existing records without weakening reads.""" + +from dataclasses import replace +from typing import Any + +import pyarrow as pa +import pytest + +from vgi.catalog.query import ( + QUERY_ENCODING, + QUERY_EXPLICIT, + QUERY_STATEMENT, + STATEMENT_RESULT_KEY, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, + QueryEvaluationContext, +) +from vgi.query_parameters import QueryParameters + +from .typing_helpers import present + + +def capability() -> QueryCapabilities: + return QueryCapabilities( + encoding=QUERY_ENCODING, + semantic_profiles=[QUERY_EXPLICIT, QUERY_STATEMENT], + execution_modes=["explicit", "statement"], + backend_engine="duckdb", + backend_version="qualified-fixture", + capability_revision="statement-1", + default_evaluation_context=QueryEvaluationContext(), + ) + + +def request() -> CatalogQueryPrepareRequest: + return CatalogQueryPrepareRequest( + attach_opaque_data=b"attachment", + transaction_opaque_data=b"transaction", + encoding=QUERY_ENCODING, + semantic_profile=QUERY_STATEMENT, + mode="statement", + capability_revision="statement-1", + query="DELETE FROM orders", + parameter_schema=QueryParameters.empty().schema_bytes(), + evaluation_context=QueryEvaluationContext(), + ) + + +def accepted(req: CatalogQueryPrepareRequest, schema: pa.Schema) -> CatalogQueryPrepareResult: + return CatalogQueryPrepareResult( + status="accepted", + prepared_query_token=b"preparation", + output_schema=schema.serialize().to_pybytes(), + result_ordering="unspecified", + context_digest=req.evaluation_context.digest(req.encoding, req.semantic_profile), + expires_at_epoch_ms=2_000_000_000_000, + ) + + +def test_statements_require_explicit_offer_and_transactions() -> None: + offered = ClientQueryCapabilities( + encodings=[QUERY_ENCODING], + semantic_profiles=[QUERY_EXPLICIT, QUERY_STATEMENT], + engine_version="fixture", + ) + assert present(capability().select(offered, supports_transactions=True)).execution_modes == [ + "explicit", + "statement", + ] + assert present(capability().select(offered, supports_transactions=False)).execution_modes == ["explicit"] + assert ( + capability().select(replace(offered, semantic_profiles=[QUERY_STATEMENT]), supports_transactions=False) is None + ) + assert present( + capability().select(replace(offered, semantic_profiles=[QUERY_EXPLICIT]), supports_transactions=True) + ).execution_modes == ["explicit"] + assert capability().select(None, supports_transactions=True) is None + + +def test_statement_context_owner_and_mode_are_not_inferred_from_sql() -> None: + req = request() + req.validate_agreement(capability()) + invalid_changes: list[dict[str, Any]] = [ + {"transaction_opaque_data": None}, + {"transaction_opaque_data": b""}, + {"semantic_profile": QUERY_EXPLICIT}, + {"mode": "explicit"}, + ] + for changes in invalid_changes: + with pytest.raises(ValueError): + replace(req, **changes) + with pytest.raises(ValueError, match="provider evaluation context"): + replace(req, evaluation_context=QueryEvaluationContext(timezone="Etc/UTC")).validate_agreement(capability()) + with pytest.raises(ValueError, match="agreement"): + req.validate_agreement(replace(capability(), semantic_profiles=[QUERY_EXPLICIT], execution_modes=["explicit"])) + + +@pytest.mark.parametrize("kind", [b"command", b"rows"]) +def test_statement_preparation_uses_existing_record_and_schema_carriers(kind: bytes) -> None: + req = request() + field = pa.field("affected_rows", pa.int64()) if kind == b"command" else pa.field("id", pa.int32()) + result = accepted(req, pa.schema([field], metadata={STATEMENT_RESULT_KEY: kind})) + result.validate_request(req) + assert CatalogQueryPrepareRequest.deserialize_from_bytes(req.serialize_to_bytes()) == req + assert CatalogQueryPrepareResult.deserialize_from_bytes(result.serialize_to_bytes()) == result + read_request = replace(req, mode="explicit", semantic_profile=QUERY_EXPLICIT) + read_result = replace(result, context_digest=read_request.evaluation_context.digest(QUERY_ENCODING, QUERY_EXPLICIT)) + with pytest.raises(ValueError, match="statement profile"): + read_result.validate_request(read_request) + + +@pytest.mark.parametrize( + "schema", + [ + pa.schema([pa.field("affected_rows", pa.int64())]), + pa.schema([pa.field("affected_rows", pa.int64())], metadata={STATEMENT_RESULT_KEY: b"unknown"}), + pa.schema([pa.field("count", pa.int64())], metadata={STATEMENT_RESULT_KEY: b"command"}), + pa.schema([pa.field("affected_rows", pa.int32())], metadata={STATEMENT_RESULT_KEY: b"command"}), + pa.schema([pa.field("affected_rows", pa.int64(), nullable=False)], metadata={STATEMENT_RESULT_KEY: b"command"}), + pa.schema([], metadata={STATEMENT_RESULT_KEY: b"rows"}), + ], +) +def test_invalid_statement_results_reject_during_preparation(schema: pa.Schema) -> None: + req = request() + with pytest.raises(ValueError, match="statement"): + accepted(req, schema).validate_request(req) + + +@pytest.mark.parametrize("second", [b"command", b"rows"]) +def test_duplicate_result_kind_metadata_rejects(second: bytes) -> None: + schema = pa.schema( + [pa.field("affected_rows", pa.int64())], + metadata=pa.KeyValueMetadata([(STATEMENT_RESULT_KEY, b"command"), (STATEMENT_RESULT_KEY, second)]), # type: ignore[arg-type] # Arrow supports duplicate metadata pairs. + ) + req = request() + with pytest.raises(ValueError, match="metadata keys must be unique"): + accepted(req, schema).validate_request(req) diff --git a/tests/catalog/test_statement_protocol.py b/tests/catalog/test_statement_protocol.py new file mode 100644 index 00000000..f3adc3c0 --- /dev/null +++ b/tests/catalog/test_statement_protocol.py @@ -0,0 +1,591 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Real DuckDB transactions through signed HTTP and existing query streaming RPCs. + +The in-process HTTP fixture exercises real protocol serialization and sealing, +not a mock SQL backend. Its one process owns transaction connections: these +tests do not qualify portable owners, production recovery or native writes. +""" + +from collections.abc import Iterator +from dataclasses import replace +from pathlib import Path +from typing import Any, NoReturn + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient +from vgi_rpc.rpc import AuthContext, CallContext + +from vgi.arguments import Arguments +from vgi.catalog import CatalogAttachResult, TableInfo, TransactionOpaqueData +from vgi.catalog.query import ( + QUERY_ENCODING, + QUERY_EXECUTOR, + QUERY_EXPLICIT, + QUERY_STATEMENT, + STATEMENT_RESULT_KEY, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + QueryEvaluationContext, + read_query_schema, +) +from vgi.client import Client +from vgi.invocation import BindResponse, FunctionType +from vgi.protocol import BindRequest, ClientCapabilities, VgiProtocol +from vgi.query_parameters import QueryParameters +from vgi.query_provider import QueryExecution, QueryOwner, QueryPreparation +from vgi.serve import create_app +from vgi.table_function import TableInOutFunctionInitPhase + +from .statement_reference import ( + BASELINE, + COMMAND, + CREATE, + DELETE, + DROP, + INSERT, + INSERT_RETURNING, + OFFER, + RETURNING, + REVISION, + SELECT, + SIGNING_KEY, + UPDATE, + UPDATE_RETURNING, + StatementAuthority, + StatementReferenceCatalog, + StatementReferenceProvider, + StatementReferenceWorker, +) +from .typing_helpers import present + +CAPS = ClientCapabilities( + engine="fixture", native_formats=[], catalogs=[], can_stream=True, filter_encodings=[], query_pushdown=OFFER +) +CTX = CallContext(auth=AuthContext.anonymous(), emit_client_log=lambda *args, **kwargs: None) + + +@pytest.fixture +def backend( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> Iterator[tuple[StatementReferenceWorker, StatementAuthority]]: + authority = StatementAuthority(tmp_path / "statements.duckdb") + monkeypatch.setattr(StatementReferenceCatalog, "authority", authority) + worker = StatementReferenceWorker(quiet=True) + worker._signing_key = SIGNING_KEY + try: + yield worker, authority + finally: + authority.close() + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", + httpx_client=_SyncTestClient(create_app(StatementReferenceWorker, describe=False, signing_key=SIGNING_KEY)), + ) + + +def attach(c: Client, capabilities: ClientCapabilities = CAPS) -> CatalogAttachResult: + return c.catalog_attach( + name="statements", data_version_spec=None, implementation_version=None, client_capabilities=capabilities + ) + + +def parameters(**values: int | str | None) -> QueryParameters: + if not values: + return QueryParameters.empty(mode="named") + schema = pa.schema([(name, pa.int64() if name == "id" else pa.string()) for name in values]) + return QueryParameters.from_batch(pa.RecordBatch.from_pylist([values], schema=schema), mode="named") + + +def request( + attached: CatalogAttachResult, + tx: bytes | None, + query: str, + values: QueryParameters | None = None, + *, + read_only: bool = False, +) -> CatalogQueryPrepareRequest: + return CatalogQueryPrepareRequest( + attach_opaque_data=attached.attach_opaque_data, + transaction_opaque_data=tx, + encoding=QUERY_ENCODING, + semantic_profile=QUERY_EXPLICIT if read_only else QUERY_STATEMENT, + mode="explicit" if read_only else "statement", + capability_revision=REVISION, + query=query, + parameter_schema=(values or parameters()).schema_bytes(), + evaluation_context=QueryEvaluationContext(), + ) + + +def bind_request( + attached: CatalogAttachResult, tx: bytes, prepared: CatalogQueryPrepareResult, values: QueryParameters +) -> BindRequest: + return BindRequest( + function_name=QUERY_EXECUTOR, + schema_path=[], + function_type=FunctionType.TABLE, + attach_opaque_data=attached.attach_opaque_data, + transaction_opaque_data=tx, + arguments=Arguments( + positional=( + pa.scalar(prepared.prepared_query_token, type=pa.binary()), + pa.scalar(values.serialize_to_bytes(), type=pa.binary()), + ) + ), + ) + + +def execute( + c: Client, attached: CatalogAttachResult, tx: bytes, query: str, values: QueryParameters | None = None +) -> pa.Table: + values = values or parameters() + prepared = c.catalog_query_prepare(request(attached, tx, query, values, read_only=query == SELECT)) + assert prepared.status == "accepted" + batches = list( + c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, + prepared=prepared, + parameters=values, + transaction_opaque_data=tx, + ) + ) + return pa.Table.from_batches(batches, schema=read_query_schema(present(prepared.output_schema))) + + +def begin(c: Client, attached: CatalogAttachResult) -> bytes: + return present(c.catalog_transaction_begin(attach_opaque_data=attached.attach_opaque_data)) + + +def finish(c: Client, attached: CatalogAttachResult, tx: bytes, operation: str = "rollback") -> None: + getattr(c, f"catalog_transaction_{operation}")( + attach_opaque_data=attached.attach_opaque_data, transaction_opaque_data=tx + ) + + +def committed_rows(authority: StatementAuthority) -> list[tuple[Any, ...]]: + return authority.connection.execute(SELECT).fetchall() + + +def table(c: Client, attached: CatalogAttachResult, tx: bytes | None = None) -> TableInfo | None: + return c.table_get( + attach_opaque_data=attached.attach_opaque_data, + transaction_opaque_data=None if tx is None else TransactionOpaqueData(tx), + schema_path=["main"], + name="added", + ) + + +def version(c: Client, attached: CatalogAttachResult, tx: bytes | None = None) -> int: + return c.catalog_version( + attach_opaque_data=attached.attach_opaque_data, + transaction_opaque_data=None if tx is None else TransactionOpaqueData(tx), + ) + + +@pytest.mark.parametrize("query", [INSERT, INSERT_RETURNING]) +@pytest.mark.parametrize("completion", ["commit", "rollback"]) +def test_typed_null_prepare_bind_release_and_real_transaction_effects( + backend: tuple[StatementReferenceWorker, StatementAuthority], query: str, completion: str +) -> None: + worker, authority = backend + with client() as c: + attached = attach(c) + assert present(attached.query_pushdown).execution_modes == ["explicit", "statement"] + tx = begin(c, attached) + values = parameters(id=10, label=None) + prepared = c.catalog_query_prepare(request(attached, tx, query, values)) + bound = worker.bind(bind_request(attached, tx, prepared, values), CTX) + expected = RETURNING if query == INSERT_RETURNING else COMMAND + assert bound.output_schema.equals(expected, check_metadata=True) + assert authority.starts == 0 + assert committed_rows(authority) == BASELINE + assert next(iter(authority.transactions.values())).connection.execute(SELECT).fetchall() == BASELINE + batches = list( + c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, + prepared=prepared, + parameters=values, + transaction_opaque_data=tx, + ) + ) + result = pa.Table.from_batches(batches) + assert result.schema.equals(expected, check_metadata=True) + assert result.to_pydict() == ( + {"id": [10], "label": [None]} if query == INSERT_RETURNING else {"affected_rows": [1]} + ) + assert authority.starts == 1 + assert not authority.executions + assert committed_rows(authority) == BASELINE # EOS is not COMMIT. + for _ in range(2): + c.catalog_query_release( + attach_opaque_data=attached.attach_opaque_data, + transaction_opaque_data=tx, + prepared_query_token=present(prepared.prepared_query_token), + ) + assert not authority.preparations + finish(c, attached, tx, completion) + assert not authority.transactions + # Reconnect through public RPC, while the one provider process still owns the database. + with client() as c: + attached = attach(c) + tx = begin(c, attached) + result = execute(c, attached, tx, SELECT) + expected_rows = BASELINE + ([(10, None)] if completion == "commit" else []) + assert list(zip(result.column(0).to_pylist(), result.column(1).to_pylist(), strict=True)) == expected_rows + finish(c, attached, tx) + + +def test_update_delete_counts_and_empty_returning(backend: tuple[StatementReferenceWorker, StatementAuthority]) -> None: + _, authority = backend + with client() as c: + attached = attach(c) + tx = begin(c, attached) + assert execute(c, attached, tx, UPDATE, parameters(id=0, label="changed")).to_pydict() == {"affected_rows": [1]} + assert execute(c, attached, tx, DELETE, parameters(id=0)).to_pydict() == {"affected_rows": [1]} + assert execute(c, attached, tx, DELETE, parameters(id=0)).to_pydict() == {"affected_rows": [0]} + assert execute(c, attached, tx, UPDATE_RETURNING, parameters(label="all")).num_rows == 2 + for value in [1, 2]: + execute(c, attached, tx, DELETE, parameters(id=value)) + empty = execute(c, attached, tx, UPDATE_RETURNING, parameters(label="none")) + assert empty.num_rows == 0 and empty.schema.metadata[STATEMENT_RESULT_KEY] == b"rows" + assert not authority.executions + finish(c, attached, tx, "commit") + assert committed_rows(authority) == [] + + +@pytest.mark.parametrize("completion", ["commit", "rollback"]) +def test_ddl_is_transactional_and_existing_version_rpc_sees_new_metadata( + backend: tuple[StatementReferenceWorker, StatementAuthority], completion: str +) -> None: + worker, authority = backend + with client() as c: + attached = attach(c) + tx = begin(c, attached) + prepared = c.catalog_query_prepare(request(attached, tx, CREATE)) + worker.bind(bind_request(attached, tx, prepared, parameters()), CTX) + assert table(c, attached, tx) is None and authority.starts == 0 + assert prepared.validated_catalog_version == version(c, attached, tx) == 1 + result = execute(c, attached, tx, CREATE) + assert result.to_pydict() == {"affected_rows": [None]} + assert result.schema.equals(COMMAND, check_metadata=True) + assert present(table(c, attached, tx)).name == "added" + assert version(c, attached, tx) == 2 + assert table(c, attached) is None and version(c, attached) == 1 + # Preparation describes the old snapshot; it is never mutated into a completion receipt. + assert prepared.validated_catalog_version == 1 + finish(c, attached, tx, completion) + assert (table(c, attached) is not None) == (completion == "commit") + assert version(c, attached) == (2 if completion == "commit" else 1) + if completion == "commit": + tx = begin(c, attached) + assert execute(c, attached, tx, DROP).to_pydict() == {"affected_rows": [None]} + assert table(c, attached, tx) is None and version(c, attached, tx) == 3 + finish(c, attached, tx) + assert table(c, attached) is not None and version(c, attached) == 2 + + +def test_statement_profile_does_not_authorize_read_profile_mutation_or_scripts( + backend: tuple[StatementReferenceWorker, StatementAuthority], +) -> None: + _, authority = backend + with client() as c: + attached = attach(c) + tx = begin(c, attached) + declined = c.catalog_query_prepare(request(attached, tx, INSERT, parameters(id=10, label="x"), read_only=True)) + assert declined.status == "declined" + for sql in ["BEGIN", "COMMIT", "ROLLBACK", "SET threads=8", CREATE + "; " + INSERT, "COPY items TO 'x.csv'"]: + assert c.catalog_query_prepare(request(attached, tx, sql)).status == "declined" + with pytest.raises(ValueError, match="transaction"): + request(attached, None, CREATE) + assert authority.starts == 0 and committed_rows(authority) == BASELINE + finish(c, attached, tx, "commit") + + +def test_unoffered_statement_and_wrong_owner_reject_before_effects( + backend: tuple[StatementReferenceWorker, StatementAuthority], +) -> None: + _, authority = backend + with client() as c: + readonly = attach(c, replace(CAPS, query_pushdown=replace(OFFER, semantic_profiles=[QUERY_EXPLICIT]))) + readonly_tx = begin(c, readonly) + with pytest.raises(Exception, match="agreement|profile|capability"): + c.catalog_query_prepare(request(readonly, readonly_tx, CREATE)) + finish(c, readonly, readonly_tx) + attached, other = attach(c), attach(c) + tx, other_tx = begin(c, attached), begin(c, other) + values = parameters(id=10, label="x") + prepared = c.catalog_query_prepare(request(attached, tx, INSERT, values)) + for owner, token in [(other, other_tx), (attached, other_tx), (attached, None)]: + with pytest.raises(Exception, match="opaque|query|transaction"): + list( + c.catalog_query_execute( + attach_opaque_data=owner.attach_opaque_data, + prepared=prepared, + parameters=values, + transaction_opaque_data=token, + ) + ) + wrong = QueryParameters.from_batch( + pa.record_batch([pa.array([10], type=pa.int32()), pa.array(["x"])], names=["id", "label"]), mode="named" + ) + with pytest.raises(Exception, match="schema"): + list( + c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, + prepared=prepared, + parameters=wrong, + transaction_opaque_data=tx, + ) + ) + assert authority.starts == 0 and committed_rows(authority) == BASELINE + finish(c, attached, tx, "commit") + finish(c, other, other_tx) + + +@pytest.mark.parametrize("failure", ["cancel", "result_error", "wrong_result_schema", "expired_result"]) +def test_incomplete_result_poisons_whole_transaction_and_rollback_undoes_prior_statements( + backend: tuple[StatementReferenceWorker, StatementAuthority], failure: str +) -> None: + _, authority = backend + with client() as c: + attached = attach(c) + tx = begin(c, attached) + execute(c, attached, tx, INSERT, parameters(id=10, label="earlier")) + if failure == "result_error": + authority.fail_read_at = 1 + values = parameters(label="mutated") + prepared = c.catalog_query_prepare(request(attached, tx, UPDATE_RETURNING, values)) + stream = c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, + prepared=prepared, + parameters=values, + transaction_opaque_data=tx, + ) + assert next(stream).num_rows == 2 + assert authority.starts == 2 + with pytest.raises(Exception, match="pending|rollback"): + finish(c, attached, tx, "commit") + if failure == "cancel": + stream.close() + else: + result = next(iter(authority.executions.values())) + if failure == "wrong_result_schema": + result.batches[1] = pa.record_batch([pa.array(["bad"])], names=["wrong"]) + elif failure == "expired_result": + result.expires = 0 + with pytest.raises(Exception, match="failure|schema|expired"): + list(stream) + assert next(iter(authority.transactions.values())).failed + with pytest.raises(Exception, match="failed|rollback"): + finish(c, attached, tx, "commit") + assert authority.starts == 2 # No retry after effects or local fallback. + finish(c, attached, tx) + assert committed_rows(authority) == BASELINE + assert not authority.executions and not authority.preparations + + +def test_backend_execution_error_requires_rollback_of_prior_dml_and_ddl( + backend: tuple[StatementReferenceWorker, StatementAuthority], +) -> None: + _, authority = backend + with client() as c: + attached = attach(c) + tx = begin(c, attached) + execute(c, attached, tx, INSERT, parameters(id=10, label="earlier")) + execute(c, attached, tx, CREATE) + with pytest.raises(Exception, match="already exists"): + execute(c, attached, tx, CREATE) + assert authority.starts == 3 + with pytest.raises(Exception, match="failed|rollback"): + finish(c, attached, tx, "commit") + finish(c, attached, tx) + assert committed_rows(authority) == BASELINE + assert table(c, attached) is None and version(c, attached) == 1 + + +@pytest.mark.parametrize( + "malformed", + ["missing_row", "empty_batch", "two_rows", "extra_row", "extra_empty_batch", "negative_count", "changed_schema"], +) +def test_malformed_command_completion_invalidates_transaction_after_real_effect( + backend: tuple[StatementReferenceWorker, StatementAuthority], monkeypatch: pytest.MonkeyPatch, malformed: str +) -> None: + _, authority = backend + original = StatementReferenceProvider.start + + def malformed_result( + self: StatementReferenceProvider, + *, + owner: QueryOwner, + preparation: QueryPreparation, + parameters: QueryParameters, + execution_id: bytes, + ) -> QueryExecution: + execution = original( + self, owner=owner, preparation=preparation, parameters=parameters, execution_id=execution_id + ) + result = authority.executions[execution.handle] + valid = result.batches[0] + variants = { + "missing_row": [], + "empty_batch": [valid.slice(0, 0)], + "two_rows": [pa.record_batch([pa.array([1, 1], type=pa.int64())], schema=COMMAND)], + "extra_row": [valid, valid], + "extra_empty_batch": [valid, valid.slice(0, 0)], + "negative_count": [pa.record_batch([pa.array([-1], type=pa.int64())], schema=COMMAND)], + "changed_schema": [valid.replace_schema_metadata(None)], + } + result.batches = variants[malformed] + return execution + + monkeypatch.setattr(StatementReferenceProvider, "start", malformed_result) + with client() as c: + attached = attach(c) + tx = begin(c, attached) + with pytest.raises(Exception, match="completion row|schema|affected_rows"): + execute(c, attached, tx, INSERT, parameters(id=10, label="effect already occurred")) + assert authority.starts == 1 + assert not authority.executions + transaction = next(iter(authority.transactions.values())) + assert transaction.failed + assert transaction.connection.execute(SELECT).fetchall() == BASELINE + [(10, "effect already occurred")] + with pytest.raises(Exception, match="failed|rollback"): + finish(c, attached, tx, "commit") + finish(c, attached, tx) + assert committed_rows(authority) == BASELINE + assert not authority.transactions and not authority.preparations + + +def test_lost_init_reply_simulation_does_not_retry_and_pending_execution_prevents_commit( + backend: tuple[StatementReferenceWorker, StatementAuthority], monkeypatch: pytest.MonkeyPatch +) -> None: + _, authority = backend + with client() as c: + attached = attach(c) + tx = begin(c, attached) + values = parameters(id=10, label="once") + prepared = c.catalog_query_prepare(request(attached, tx, INSERT, values)) + original = c._do_init + opened = [] + + def lose_reply( + proxy: VgiProtocol, + bind_request: BindRequest, + bind_response: BindResponse, + *, + projection_ids: list[int] | None = None, + pushdown_filters_batch: pa.RecordBatch | None = None, + phase: TableInOutFunctionInitPhase | None = None, + execution_id: bytes | None = None, + init_opaque_data: bytes | None = None, + finalize_state_id: bytes | None = None, + split_tokens: list[bytes] | None = None, + join_keys: list[pa.RecordBatch] | None = None, + substream_id: bytes | None = None, + ) -> NoReturn: + # Real signed-HTTP init succeeds, but the application never receives its + # returned stream. This simulates the ambiguous client boundary, not + # a packet-loss transport or a provider's crash recovery. + opened.append( + original( + proxy, + bind_request, + bind_response, + projection_ids=projection_ids, + pushdown_filters_batch=pushdown_filters_batch, + phase=phase, + execution_id=execution_id, + init_opaque_data=init_opaque_data, + finalize_state_id=finalize_state_id, + split_tokens=split_tokens, + join_keys=join_keys, + substream_id=substream_id, + ) + ) + raise OSError("simulated lost init reply") + + monkeypatch.setattr(c, "_do_init", lose_reply) + with pytest.raises(OSError, match="lost init reply"): + list( + c.catalog_query_execute( + attach_opaque_data=attached.attach_opaque_data, + prepared=prepared, + parameters=values, + transaction_opaque_data=tx, + ) + ) + assert authority.starts == 1 and len(authority.executions) == 1 + with pytest.raises(Exception, match="pending|rollback"): + finish(c, attached, tx, "commit") + finish(c, attached, tx) + for stream in opened: + stream.close() + assert committed_rows(authority) == BASELINE and not authority.executions + + +@pytest.mark.parametrize("operation", ["commit", "rollback"]) +def test_completion_is_owner_bound_and_idempotent_without_reexecuting( + backend: tuple[StatementReferenceWorker, StatementAuthority], operation: str +) -> None: + _, authority = backend + with client() as c: + attached, other = attach(c), attach(c) + tx = begin(c, attached) + execute(c, attached, tx, INSERT, parameters(id=10, label="once")) + finish(c, attached, tx, operation) + finish(c, attached, tx, operation) + assert authority.starts == 1 and not authority.transactions + assert committed_rows(authority) == BASELINE + ([(10, "once")] if operation == "commit" else []) + opposite = "rollback" if operation == "commit" else "commit" + with pytest.raises(Exception, match="opposite operation"): + finish(c, attached, tx, opposite) + with pytest.raises(Exception, match="opaque|transaction|attachment"): + finish(c, other, tx, operation) + c.catalog_detach(attach_opaque_data=attached.attach_opaque_data) + assert not authority.completed + + +def test_parameter_and_result_budgets_reject_at_the_correct_effect_boundary( + backend: tuple[StatementReferenceWorker, StatementAuthority], monkeypatch: pytest.MonkeyPatch +) -> None: + _, authority = backend + with client() as c: + attached = attach(c) + tx = begin(c, attached) + with pytest.raises(Exception, match="8 KiB limit"): + execute(c, attached, tx, INSERT, parameters(id=10, label="x" * 8192)) + assert authority.starts == 0 + assert not next(iter(authority.transactions.values())).failed + finish(c, attached, tx, "commit") + tx = begin(c, attached) + monkeypatch.setattr(authority, "max_result_bytes", 1) + with pytest.raises(Exception, match="byte quota"): + execute(c, attached, tx, INSERT_RETURNING, parameters(id=10, label="inserted")) + assert authority.starts == 1 + assert next(iter(authority.transactions.values())).failed + with pytest.raises(Exception, match="failed|rollback"): + finish(c, attached, tx, "commit") + finish(c, attached, tx) + assert committed_rows(authority) == BASELINE + + +@pytest.mark.parametrize("cleanup", ["detach", "expiry"]) +def test_abandoned_owner_cleanup_rolls_back_real_effects( + backend: tuple[StatementReferenceWorker, StatementAuthority], cleanup: str +) -> None: + _, authority = backend + with client() as c: + attached = attach(c) + tx = begin(c, attached) + execute(c, attached, tx, INSERT, parameters(id=10, label="uncommitted")) + if cleanup == "detach": + c.catalog_detach(attach_opaque_data=attached.attach_opaque_data) + else: + next(iter(authority.transactions.values())).expires = 0 + authority.sweep() + assert not authority.transactions and not authority.preparations and not authority.executions + assert committed_rows(authority) == BASELINE diff --git a/tests/catalog/test_statement_socket.py b/tests/catalog/test_statement_socket.py new file mode 100644 index 00000000..5f7f9366 --- /dev/null +++ b/tests/catalog/test_statement_socket.py @@ -0,0 +1,94 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Run the bounded statement example in a separate process over real HTTP.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path +from queue import Empty, Queue +from threading import Thread + +from vgi.client import Client + +from .statement_reference import CREATE, DROP, INSERT, SELECT, UPDATE +from .test_statement_protocol import attach, begin, execute, finish, parameters, table, version + + +def test_statement_example_over_socket_http(tmp_path: Path) -> None: + env = os.environ | {"VGI_STATEMENT_REFERENCE_DIR": str(tmp_path / "provider")} + log_path = tmp_path / "provider.log" + with log_path.open("wb") as log: + server = subprocess.Popen( + [sys.executable, "-m", "tests.catalog.statement_reference", "--port", "0"], + cwd=Path(__file__).resolve().parents[2], + env=env, + stdout=subprocess.PIPE, + stderr=log, + ) + reader: Thread | None = None + try: + stdout = server.stdout + assert stdout is not None + readiness: Queue[str] = Queue(maxsize=1) + + def read_readiness() -> None: + # Windows selectors accept sockets, not subprocess pipes. Keep + # the same bounded startup deadline on every platform. + readiness.put(stdout.readline().decode(errors="replace").strip()) + + reader = Thread(target=read_readiness, daemon=True) + reader.start() + try: + announced = readiness.get(timeout=30) + except Empty: + raise AssertionError( + f"statement example did not start (exit={server.poll()}); " + f"stderr:\n{log_path.read_text(errors='replace')}" + ) from None + assert announced.startswith("PORT:"), ( + f"unexpected readiness line: {announced!r} (exit={server.poll()}); " + f"stderr:\n{log_path.read_text(errors='replace')}" + ) + url = f"http://127.0.0.1:{int(announced.split(':')[1])}" + with Client.from_http(url) as client: + attached = attach(client) + tx = begin(client, attached) + assert execute(client, attached, tx, CREATE).to_pydict() == {"affected_rows": [None]} + assert execute(client, attached, tx, INSERT, parameters(id=10, label="socket ' value")).to_pydict() == { + "affected_rows": [1] + } + assert table(client, attached, tx) is not None + assert version(client, attached, tx) == 2 + assert table(client, attached) is None + finish(client, attached, tx, "commit") + client.catalog_detach(attach_opaque_data=attached.attach_opaque_data) + # A new client/attachment observes committed changes and rolls back + # a later DML+DDL transaction through the same existing RPCs. + with Client.from_http(url) as client: + attached = attach(client) + assert table(client, attached) is not None and version(client, attached) == 2 + tx = begin(client, attached) + assert execute(client, attached, tx, SELECT).to_pydict()["label"][-1] == "socket ' value" + execute(client, attached, tx, UPDATE, parameters(id=10, label="rolled back")) + execute(client, attached, tx, DROP) + assert table(client, attached, tx) is None + finish(client, attached, tx) + tx = begin(client, attached) + assert execute(client, attached, tx, SELECT).to_pydict()["label"][-1] == "socket ' value" + assert table(client, attached, tx) is not None and version(client, attached, tx) == 2 + finish(client, attached, tx) + client.catalog_detach(attach_opaque_data=attached.attach_opaque_data) + finally: + server.terminate() + try: + server.wait(timeout=10) + except subprocess.TimeoutExpired: + server.kill() + server.wait(timeout=10) + if reader is not None: + reader.join(timeout=10) + if server.stdout is not None: + server.stdout.close() diff --git a/tests/catalog/test_table_layout_protocol.py b/tests/catalog/test_table_layout_protocol.py new file mode 100644 index 00000000..0cb8b438 --- /dev/null +++ b/tests/catalog/test_table_layout_protocol.py @@ -0,0 +1,440 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Public table-option/layout RPCs, persistent readback and actual row rewrites.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path +from typing import Any, TypedDict, cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.catalog.capabilities import ( + ROW_SCALAR_EXPRESSION, + TABLE_LAYOUT_ALTER_REWRITE, + TRANSACTION_DDL, +) +from vgi.catalog.catalog_interface import OnConflict, SchemaObjectType, SerializedSchema, TransactionOpaqueData +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.layout import ( + LayoutChangeMode, + LayoutCoverage, + NullOrder, + SortDirection, + SortKey, + SqlExpression, + TableLayout, + TableLayoutChange, +) +from vgi.catalog.options import OptionChanges +from vgi.client import Client +from vgi.exceptions import CatalogCapabilityError +from vgi.meta_worker import MetaWorker +from vgi.protocol import CatalogAttachRequest, ClientCapabilities, TableCreateRequest, VgiProtocol +from vgi.serve import create_app +from vgi.typed_values import TypedValueBatch + +from .schema_options_reference import values +from .table_layout_reference import TABLE_PROFILES, TableLayoutCatalog, TableLayoutWorker +from .typing_helpers import present + +KEY = b"table-layout-reference-signing-key" +COLUMNS = pa.schema([pa.field("a.b", pa.int32()), pa.field("score", pa.int64())], metadata={b"owner": b"layout"}) +OFFER = ClientCapabilities( + engine="reference", native_formats=[], catalogs=[], can_stream=False, filter_encodings=[], catalog=TABLE_PROFILES +) + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TableLayoutWorker: + """Isolate persistent metadata and physical row snapshots per test.""" + monkeypatch.setattr(TableLayoutCatalog, "database_path", str(tmp_path / "tables.sqlite"), raising=False) + worker = TableLayoutWorker(quiet=True) + worker._signing_key = KEY + return worker + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(create_app(TableLayoutWorker, describe=False, signing_key=KEY)) + ) + + +def attach(c: Client, offer: ClientCapabilities | None = OFFER) -> Any: + return c.catalog_attach( + name="layout", data_version_spec=None, implementation_version=None, client_capabilities=offer + ).attach_opaque_data + + +def expr(sql: str, path: list[str] | None = None) -> SqlExpression: + return SqlExpression( + evaluation_context=QueryEvaluationContext(), + sql=sql, + profile=ROW_SCALAR_EXPRESSION, + schema_path=path or ["main"], + ) + + +def sort(sql: str, direction: SortDirection = SortDirection.DESC, nulls: NullOrder = NullOrder.LAST) -> bytes: + return SortKey(expression=expr(sql), direction=direction, null_order=nulls).serialize_to_bytes() + + +def layout() -> TableLayout: + return TableLayout(partition_by=[expr('"a.b" % 2').serialize_to_bytes()], sorted_by=[sort("abs(score)")]) + + +def args(token: bytes) -> dict[str, Any]: + return dict(attach_opaque_data=token, schema_path=["main"], name="t.with.dot") + + +def get(c: Client, token: bytes, tx: bytes | None = None) -> Any: + return c.table_get(**args(token), transaction_opaque_data=TransactionOpaqueData(tx) if tx is not None else None) + + +def create(c: Client, token: bytes, **kwargs: Any) -> None: + c.table_create(**args(token), columns=SerializedSchema(COLUMNS.serialize().to_pybytes()), **kwargs) + + +def physical(worker: TableLayoutWorker, token: bytes, tx: bytes | None = None) -> list[dict[str, Any]]: + catalog = worker._get_catalog() + assert isinstance(catalog, TableLayoutCatalog) + return catalog.physical_rows( + **args(worker._unwrap_attach(token)), + transaction_opaque_data=worker._unwrap_transaction(tx, token) if tx else None, + ) + + +def seed(worker: TableLayoutWorker, token: bytes, rows: list[dict[str, Any]]) -> None: + catalog = worker._get_catalog() + assert isinstance(catalog, TableLayoutCatalog) + catalog.seed_rows(**args(worker._unwrap_attach(token)), transaction_opaque_data=None, rows=rows) + + +def test_create_discovery_options_and_restart(backend: TableLayoutWorker) -> None: + with client() as c: + token = attach(c) + definitions = c.table_option_definitions(**args(token)) + assert definitions.alter_names == ["label", "limit"] + create(c, token, options=values(label=None), layout=layout(), comment="kept", tags={"team": "one"}) + info = get(c, token) + assert info.comment == "kept" and info.tags == {"team": "one"} + assert info.options.explicit_names == ["label"] + assert info.options.values.batch.column("label")[0].as_py() is None + assert info.layout == layout() + assert pa.ipc.read_schema(pa.py_buffer(info.columns)).equals(COLUMNS, check_metadata=True) + listed = c.schema_contents(type=SchemaObjectType.TABLE, attach_opaque_data=token, path=["main"]) + assert listed[0].layout == info.layout + c.table_alter_options(**args(token), changes=OptionChanges(set_values=values(limit=50), reset_names=["label"])) + with client() as c: + info = get(c, token) + assert info.options.explicit_names == ["limit"] + assert info.options.values.batch.column("label")[0].as_py() == "default" + assert info.options.values.batch.column("limit")[0].as_py() == 50 + assert info.layout == layout() + + +def test_future_writes_and_atomic_rewrite_rollback_commit(backend: TableLayoutWorker) -> None: + with client() as c: + token = attach(c) + create(c, token, layout=layout()) + seed(backend, token, [{"a.b": 1, "score": 2}, {"a.b": 3, "score": -5}, {"a.b": 2, "score": None}]) + original = physical(backend, token) + assert [r["score"] for r in original[0]["rows"]] == [-5, 2] + c.table_alter_layout( + **args(token), + changes=TableLayoutChange( + mode=LayoutChangeMode.FUTURE_WRITES, + partition_by=[], + sorted_by=[sort("score", SortDirection.ASC, NullOrder.FIRST)], + ), + ) + assert physical(backend, token) == original + assert get(c, token).layout.coverage == LayoutCoverage.FUTURE_WRITES + seed(backend, token, [{"a.b": 4, "score": 9}, {"a.b": 5, "score": None}, {"a.b": 6, "score": 1}]) + assert [r["score"] for r in physical(backend, token)[-1]["rows"]] == [None, 1, 9] + before = physical(backend, token) + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.table_alter_layout( + **args(token), + transaction_opaque_data=tx, + changes=TableLayoutChange(mode=LayoutChangeMode.REWRITE, partition_by=[]), + ) + assert len(physical(backend, token, tx)) == 1 + assert [r["score"] for r in physical(backend, token, tx)[0]["rows"]] == [None, None, -5, 1, 2, 9] + assert physical(backend, token) == before + with client() as c: + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert physical(backend, token) == before + tx = c.catalog_transaction_begin(attach_opaque_data=token) + c.table_alter_layout( + **args(token), + transaction_opaque_data=tx, + changes=TableLayoutChange(mode=LayoutChangeMode.REWRITE, sorted_by=[]), + ) + with client() as c: + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(tx)) + assert len(physical(backend, token)) == 1 + assert get(c, token).layout.coverage == LayoutCoverage.ALL_ROWS + c.table_drop(**args(token)) + assert get(c, token) is None + + +@pytest.mark.parametrize("sql", ["random()", "(select 42)", "missing_column", "1); DROP TABLE input_data; SELECT (1"]) +def test_invalid_expression_rejected_before_catalog_change(backend: TableLayoutWorker, sql: str) -> None: + with client() as c: + token = attach(c) + version = c.catalog_version(attach_opaque_data=token) + with pytest.raises(Exception, match="scalar|expression|column|syntax"): + create(c, token, layout=TableLayout(sorted_by=[sort(sql)])) + assert get(c, token) is None + assert c.catalog_version(attach_opaque_data=token) == version + + +def test_rewrite_evaluation_failure_preserves_metadata_and_rows(backend: TableLayoutWorker) -> None: + with client() as c: + token = attach(c) + create(c, token) + seed(backend, token, [{"a.b": 1, "score": -(2**63)}]) + before, version = physical(backend, token), c.catalog_version(attach_opaque_data=token) + with pytest.raises(Exception, match="overflow|Overflow|range"): + c.table_alter_layout( + **args(token), changes=TableLayoutChange(mode=LayoutChangeMode.REWRITE, sorted_by=[sort("abs(score)")]) + ) + assert physical(backend, token) == before + assert get(c, token).layout == TableLayout() + assert c.catalog_version(attach_opaque_data=token) == version + + +def test_legacy_projection_and_capability_scoped_etags(backend: TableLayoutWorker) -> None: + with client() as c: + token = attach(c) + old = attach(c, None) + create(c, token, layout=layout(), options=values(limit=19)) + assert get(c, old).layout is None and get(c, old).options is None + assert c.schema_contents(type=SchemaObjectType.TABLE, attach_opaque_data=old, path=["main"])[0].layout is None + with c._catalog_connect() as proxy: + full = proxy.catalog_contents(attach_opaque_data=token) + legacy = proxy.catalog_contents(attach_opaque_data=old, if_none_match=full.etag) + assert not legacy.not_modified and legacy.etag != full.etag + assert proxy.catalog_contents(attach_opaque_data=old, if_none_match=legacy.etag).not_modified + from vgi.catalog import TableInfo + from vgi.catalog.contents_codec import decode_collection + + assert decode_collection(TableInfo, legacy.tables)[0].layout is None + + +class _CreateExtra(TypedDict, total=False): + """Independent create fields whose capabilities must be checked.""" + + options: TypedValueBatch + layout: TableLayout + comment: str + tags: dict[str, str] + + +def test_missing_profiles_and_mode_downgrade_never_call_provider( + backend: TableLayoutWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + token = backend.catalog_attach( + CatalogAttachRequest( + name="layout", data_version_spec=None, implementation_version=None, client_capabilities=None + ) + ).attach_opaque_data + calls = [] + monkeypatch.setattr(backend._get_catalog(), "table_create", lambda **kw: calls.append(kw)) + extras: list[_CreateExtra] = [{"options": values(limit=1)}, {"layout": layout()}, {"comment": "c"}, {"tags": {}}] + for extra in extras: + with pytest.raises(CatalogCapabilityError): + backend.catalog_table_create( + TableCreateRequest( + constraint_definitions=[], + **args(token), + columns=COLUMNS.serialize().to_pybytes(), + on_conflict=OnConflict.ERROR, + **extra, + ) + ) + assert not calls + token = backend.catalog_attach( + CatalogAttachRequest( + name="layout", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ) + ).attach_opaque_data + monkeypatch.setattr( + backend._get_catalog(), + "profiles", + replace(TABLE_PROFILES, ddl=[p for p in TABLE_PROFILES.ddl if p != TABLE_LAYOUT_ALTER_REWRITE]), + ) + monkeypatch.setattr(backend._get_catalog(), "table_alter_layout", lambda **kw: calls.append(kw)) + with pytest.raises(CatalogCapabilityError): + backend.catalog_table_alter_layout( + **args(token), changes=TableLayoutChange(mode=LayoutChangeMode.REWRITE, sorted_by=[]).serialize_to_bytes() + ) + assert not calls + + +def test_new_table_ddl_requires_transaction_guarantee(backend: TableLayoutWorker) -> None: + limited = replace(OFFER, catalog=replace(TABLE_PROFILES, transaction=[])) + token = backend.catalog_attach( + CatalogAttachRequest( + name="layout", data_version_spec=None, implementation_version=None, client_capabilities=limited + ) + ).attach_opaque_data + tx = backend.catalog_transaction_begin(token).transaction_opaque_data + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + backend.catalog_table_create( + TableCreateRequest( + constraint_definitions=[], + **args(token), + transaction_opaque_data=tx, + columns=COLUMNS.serialize().to_pybytes(), + on_conflict=OnConflict.ERROR, + layout=layout(), + ) + ) + backend.catalog_transaction_rollback(token, present(tx)) + + +def test_layout_mode_and_nested_record_validation() -> None: + with pytest.raises(ValueError): + TableLayoutChange(mode=LayoutChangeMode.REWRITE) + with pytest.raises(ValueError): + SortKey(expression=expr("score"), direction=cast(SortDirection, "default"), null_order=NullOrder.LAST) + with pytest.raises(pa.ArrowInvalid): + TableLayout(partition_by=[b"not IPC"]) + assert ( + TableLayoutChange(mode=LayoutChangeMode.REWRITE, sorted_by=[]).apply_to(layout()).partition_by + == layout().partition_by + ) + + +def test_partial_layout_change_rechecks_retained_expression_profiles( + backend: TableLayoutWorker, monkeypatch: pytest.MonkeyPatch +) -> None: + with client() as c: + token = attach(c) + create(c, token, layout=layout()) + seed(backend, token, [{"a.b": 1, "score": 2}]) + before = physical(backend, token) + version = c.catalog_version(attach_opaque_data=token) + no_expressions = replace( + OFFER, catalog=replace(TABLE_PROFILES, read=[p for p in TABLE_PROFILES.read if p != ROW_SCALAR_EXPRESSION]) + ) + limited = attach(c, no_expressions) + assert get(c, limited).layout is None + calls = [] + monkeypatch.setattr(backend._get_catalog(), "table_alter_layout", lambda **kw: calls.append(kw)) + change = TableLayoutChange(mode=LayoutChangeMode.REWRITE, partition_by=[]) + with pytest.raises(CatalogCapabilityError, match=ROW_SCALAR_EXPRESSION): + backend.catalog_table_alter_layout(**args(limited), changes=change.serialize_to_bytes()) + monkeypatch.setattr(backend._get_catalog(), "profiles", no_expressions.catalog) + with pytest.raises(CatalogCapabilityError, match=ROW_SCALAR_EXPRESSION): + backend.catalog_table_alter_layout(**args(token), changes=change.serialize_to_bytes()) + assert not calls and physical(backend, token) == before + assert c.catalog_version(attach_opaque_data=token) == version + + +def test_invalid_mixed_option_changes_are_atomic(backend: TableLayoutWorker) -> None: + with client() as c: + token = attach(c) + create(c, token, options=values(limit=19)) + before, version = get(c, token), c.catalog_version(attach_opaque_data=token) + with pytest.raises(ValueError, match="unknown"): + backend.catalog_table_alter_options( + **args(token), + changes=OptionChanges(set_values=values(limit=25), reset_names=["unknown"]).serialize_to_bytes(), + ) + assert get(c, token) == before + assert c.catalog_version(attach_opaque_data=token) == version + + +def test_nested_owner_paths_do_not_collapse_dotted_names(backend: TableLayoutWorker) -> None: + with client() as c: + token = attach(c) + c.schema_create(attach_opaque_data=token, path=["one"]) + for path in (["one.two"], ["one", "two"]): + c.schema_create(attach_opaque_data=token, path=path) + c.table_create( + attach_opaque_data=token, + schema_path=path, + name="t", + columns=SerializedSchema(COLUMNS.serialize().to_pybytes()), + layout=TableLayout(partition_by=[expr('"a.b"', path).serialize_to_bytes()]), + ) + first = c.table_get(attach_opaque_data=token, schema_path=["one.two"], name="t") + second = c.table_get(attach_opaque_data=token, schema_path=["one", "two"], name="t") + assert present(present(first).layout).partition_expressions()[0].schema_path == ["one.two"] + assert present(present(second).layout).partition_expressions()[0].schema_path == ["one", "two"] + version = c.catalog_version(attach_opaque_data=token) + with pytest.raises(ValueError, match="owner schema"): + backend.catalog_table_create( + TableCreateRequest( + constraint_definitions=[], + attach_opaque_data=token, + schema_path=["one.two"], + name="bad", + columns=COLUMNS.serialize().to_pybytes(), + on_conflict=OnConflict.ERROR, + layout=TableLayout(partition_by=[expr('"a.b"', ["one", "two"]).serialize_to_bytes()]), + ) + ) + assert c.catalog_version(attach_opaque_data=token) == version + + +def test_meta_worker_routes_all_table_option_and_layout_operations(backend: TableLayoutWorker) -> None: + # MetaWorker installs protocol forwarding methods dynamically. + worker = cast(VgiProtocol, MetaWorker([backend])) + token = worker.catalog_attach( + CatalogAttachRequest( + name="layout", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ) + ).attach_opaque_data + worker.catalog_table_create( + TableCreateRequest( + constraint_definitions=[], + **args(token), + columns=COLUMNS.serialize().to_pybytes(), + on_conflict=OnConflict.ERROR, + layout=layout(), + ) + ) + assert worker.catalog_table_option_definitions(**args(token)).alter_names == ["label", "limit"] + worker.catalog_table_alter_options( + **args(token), changes=OptionChanges(set_values=values(limit=27)).serialize_to_bytes() + ) + worker.catalog_table_alter_layout( + **args(token), + changes=TableLayoutChange(mode=LayoutChangeMode.REWRITE, partition_by=[], sorted_by=[]).serialize_to_bytes(), + ) + info = worker.catalog_table_get(**args(token)).to_optional() + assert info.options.values.batch.column("limit")[0].as_py() == 27 + assert info.layout == TableLayout() + + +def test_conflicting_layout_commit_preserves_winner(backend: TableLayoutWorker) -> None: + with client() as c: + token = attach(c) + create(c, token, layout=layout()) + seed(backend, token, [{"a.b": 1, "score": 2}]) + before = physical(backend, token) + first = c.catalog_transaction_begin(attach_opaque_data=token) + second = c.catalog_transaction_begin(attach_opaque_data=token) + c.table_alter_options( + **args(token), transaction_opaque_data=first, changes=OptionChanges(set_values=values(limit=31)) + ) + c.table_alter_layout( + **args(token), + transaction_opaque_data=second, + changes=TableLayoutChange(mode=LayoutChangeMode.REWRITE, partition_by=[], sorted_by=[]), + ) + c.catalog_transaction_commit(attach_opaque_data=token, transaction_opaque_data=present(first)) + with pytest.raises(ValueError, match="conflict"): + backend.catalog_transaction_commit(token, present(second)) + assert get(c, token).layout == layout() + assert get(c, token).options.values.batch.column("limit")[0].as_py() == 31 + assert physical(backend, token) == before + c.catalog_transaction_rollback(attach_opaque_data=token, transaction_opaque_data=present(second)) diff --git a/tests/catalog/test_variant_capabilities.py b/tests/catalog/test_variant_capabilities.py new file mode 100644 index 00000000..30bac110 --- /dev/null +++ b/tests/catalog/test_variant_capabilities.py @@ -0,0 +1,27 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Canonical Variant negotiation cannot select the retired custom encoding.""" + +import pytest + +from vgi.catalog.capabilities import LOGICAL_TYPE_VARIANT, CatalogCapabilities +from vgi.exceptions import CatalogCapabilityError +from vgi.logical_types import VARIANT_PROFILE + + +def test_canonical_variant_profile_requires_matching_offer_and_provider() -> None: + """Neither a missing offer nor the old unpublished encoding confers support.""" + assert LOGICAL_TYPE_VARIANT == VARIANT_PROFILE == "vgi.type.variant.parquet.v1" + available = CatalogCapabilities(read=[LOGICAL_TYPE_VARIANT]) + retired = CatalogCapabilities(read=["vgi.type.variant.arrow.v1"]) + assert available.select(None) == CatalogCapabilities() + assert available.select(retired) == CatalogCapabilities() + assert retired.select(available) == CatalogCapabilities() + assert available.select(available) == available + + +def test_canonical_variant_profile_withdrawal_rejects_existing_agreement() -> None: + """An attachment cannot continue consuming variants after support disappears.""" + selected = CatalogCapabilities(read=[LOGICAL_TYPE_VARIANT]) + with pytest.raises(CatalogCapabilityError): + selected.require(LOGICAL_TYPE_VARIANT, available=CatalogCapabilities(), operation="read VARIANT") diff --git a/tests/catalog/test_view_metadata_cross_sdk.py b/tests/catalog/test_view_metadata_cross_sdk.py new file mode 100644 index 00000000..1fe521b8 --- /dev/null +++ b/tests/catalog/test_view_metadata_cross_sdk.py @@ -0,0 +1,234 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Run the same public RPC lifecycle against independently built SDK workers. + +Set VGI_METADATA_WORKERS to a JSON object mapping SDK names to worker argv lists. +The workers must expose the ordinary mutable ``contents_reval`` test catalog. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import replace + +import pytest + +from vgi.catalog import CatalogMetadataChanges, OnConflict, SchemaObjectType +from vgi.catalog.capabilities import CATALOG_DEPENDENCIES_READ, SCHEMA_METADATA_ALTER, CatalogCapabilities +from vgi.catalog.query import ( + QUERY_ENCODING, + QUERY_EXPLICIT, + CatalogQueryPrepareRequest, + ClientQueryCapabilities, + QueryEvaluationContext, +) +from vgi.client import Client +from vgi.protocol import ClientCapabilities +from vgi.query_parameters import QueryParameters + +from .typing_helpers import CatalogObjectArgs, CatalogSchemaArgs, present + +WORKERS: dict[str, list[str]] = json.loads(os.environ.get("VGI_METADATA_WORKERS", "{}")) + + +@pytest.mark.parametrize("sdk", sorted(WORKERS)) +def test_unqualified_provider_does_not_select_or_execute_queries(sdk: str) -> None: + offer = ClientCapabilities( + engine="reference-client", + native_formats=[], + catalogs=[], + can_stream=True, + filter_encodings=[], + query_pushdown=ClientQueryCapabilities( + encodings=[QUERY_ENCODING], semantic_profiles=[QUERY_EXPLICIT], engine_version="fixture-client-build" + ), + ) + with Client(server_path=WORKERS[sdk], worker_limit=1) as client: + attached = client.catalog_attach( + name="contents_reval", data_version_spec=None, implementation_version=None, client_capabilities=offer + ) + assert attached.query_pushdown is None + query = CatalogQueryPrepareRequest( + attach_opaque_data=attached.attach_opaque_data, + encoding=QUERY_ENCODING, + semantic_profile=QUERY_EXPLICIT, + mode="explicit", + capability_revision="unselected", + query="SELECT 1", + parameter_schema=QueryParameters.empty().schema_bytes(), + evaluation_context=QueryEvaluationContext(), + ) + with pytest.raises( + Exception, + match="(?i)unsupported|unimplemented|not implemented|does not support|not supported|capability|negotiated", + ): + client.catalog_query_prepare(query) + with pytest.raises( + Exception, + match="(?i)unsupported|unimplemented|not implemented|does not support|not supported|capability|negotiated", + ): + client.catalog_query_release(attach_opaque_data=attached.attach_opaque_data, prepared_query_token=b"absent") + + +@pytest.mark.parametrize("sdk", sorted(WORKERS)) +def test_view_metadata_lifecycle_over_public_rpc(sdk: str) -> None: + with Client(server_path=WORKERS[sdk], worker_limit=1) as client: + token = client.catalog_attach( + name="contents_reval", + data_version_spec=None, + implementation_version=None, + ).attach_opaque_data + target: CatalogObjectArgs = dict(attach_opaque_data=token, schema_path=["main"], name="view.with.dot") + client.view_create( + **target, + definition="SELECT 1 AS x, 2 AS y", + aliases=["a.b", 'with"quote'], + comment="", + tags={"owner": "one", "empty": ""}, + column_comments={"a.b": "", 'with"quote': "kept"}, + ) + original = client.view_get(**target) + original_version = client.catalog_version(attach_opaque_data=token) + assert present(original).comment == "" + assert present(original).tags == {"owner": "one", "empty": ""} + assert present(original).column_comments == {"a.b": "", 'with"quote': "kept"} + assert present(original).aliases == ["a.b", 'with"quote'] and present(original).security == "regular" + assert client.schema_contents(attach_opaque_data=token, path=["main"], type=SchemaObjectType.VIEW) == [original] + client.view_create(**target, definition="SELECT 9", on_conflict=OnConflict.IGNORE, comment="ignored") + assert client.view_get(**target) == original + assert client.catalog_version(attach_opaque_data=token) == original_version + for key in ("", "a\0b"): + for mode in (OnConflict.IGNORE, OnConflict.REPLACE): + with pytest.raises(Exception, match="identifier|alias|empty|NUL"): + client.view_create(**target, definition="SELECT 9", on_conflict=mode, column_comments={key: "bad"}) + assert client.view_get(**target) == original + assert client.catalog_version(attach_opaque_data=token) == original_version + for path in ([], ["a", "b"], [""], ["a\0b"]): + with pytest.raises(Exception, match="identifier|column path|component|empty|NUL"): + client.view_column_comment_set(**target, column_path=path, comment="bad", ignore_not_found=True) + assert client.view_get(**target) == original + assert client.catalog_version(attach_opaque_data=token) == original_version + client.view_column_comment_set(**target, column_path=["a.b"], comment=None) + client.view_column_comment_set(**target, column_path=['with"quote'], comment="") + client.view_comment_set(**target, comment=None) + assert client.view_get(**target) == replace(present(original), comment=None, column_comments={'with"quote': ""}) + client.view_create(**target, definition="SELECT 3 AS z", on_conflict=OnConflict.REPLACE) + replaced = client.view_get(**target) + assert ( + present(replaced).comment is None + and present(replaced).tags == {} + and present(replaced).column_comments == {} + ) + assert present(replaced).aliases == [] + client.view_drop(**target) + client.view_column_comment_set(**target, column_path=["a.b"], comment="unused", ignore_not_found=True) + with pytest.raises(Exception, match="not found|does not exist"): + client.view_column_comment_set(**target, column_path=["a.b"], comment="unused") + + +@pytest.mark.parametrize("sdk", sorted(WORKERS)) +def test_schema_creation_metadata_over_public_rpc(sdk: str) -> None: + with Client(server_path=WORKERS[sdk], worker_limit=1) as client: + token = client.catalog_attach( + name="contents_reval", data_version_spec=None, implementation_version=None + ).attach_opaque_data + target: CatalogSchemaArgs = dict(attach_opaque_data=token, path=["a.b"]) + tags = {"owner": "one", "empty": "", "__proto__": "literal", "a.b": "quoted"} + initial_version = client.catalog_version(attach_opaque_data=token) + client.schema_create(**target, comment="", tags=tags) + original = client.schema_get(**target) + assert present(original).comment == "" and present(original).tags == tags + assert client.catalog_version(attach_opaque_data=token) > initial_version + client.schema_create(attach_opaque_data=token, path=["a"], comment="parent") + client.schema_create(attach_opaque_data=token, path=["a", "b"], comment="nested", tags={"owner": "two"}) + assert present(client.schema_get(attach_opaque_data=token, path=["a", "b"])).comment == "nested" + assert client.schema_get(**target) == original + listed = next(s for s in client.schemas(attach_opaque_data=token) if s.path == ["a.b"]) + assert replace(listed, estimated_object_count=present(original).estimated_object_count) == original + snapshot = client.contents(attach_opaque_data=token) + cached = next(s.schema for s in snapshot.schemas if s.schema.path == ["a.b"]) + assert replace(cached, estimated_object_count=original.estimated_object_count) == original + version = client.catalog_version(attach_opaque_data=token) + with pytest.raises(Exception, match="already exists"): + client.schema_create(**target, comment="bad", tags={"bad": "bad"}) + assert client.schema_get(**target) == original + assert client.catalog_version(attach_opaque_data=token) == version + client.schema_create(**target, on_conflict=OnConflict.IGNORE, comment="ignored", tags={"ignored": "yes"}) + assert client.schema_get(**target) == original + assert client.catalog_version(attach_opaque_data=token) == version + client.schema_create(**target, on_conflict=OnConflict.REPLACE) + replacement = client.schema_get(**target) + assert present(replacement).comment is None and present(replacement).tags == {} + assert client.catalog_version(attach_opaque_data=token) > version + assert present(client.schema_get(attach_opaque_data=token, path=["a", "b"])).tags == {"owner": "two"} + client.schema_create(**target, on_conflict=OnConflict.REPLACE, comment="reset", tags=tags) + replacement = client.schema_get(**target) + assert present(replacement).comment == "reset" and present(replacement).tags == tags + snapshot = client.contents(attach_opaque_data=token) + cached = next(s.schema for s in snapshot.schemas if s.schema.path == ["a.b"]) + assert replace(cached, estimated_object_count=present(replacement).estimated_object_count) == replacement + client.schema_drop(**target) + assert client.schema_get(**target) is None + assert present(client.schema_get(attach_opaque_data=token, path=["a", "b"])).comment == "nested" + + +@pytest.mark.parametrize("sdk", sorted(WORKERS)) +def test_unqualified_provider_rejects_schema_metadata_mutation(sdk: str) -> None: + offer = ClientCapabilities( + engine="reference-client", + native_formats=[], + catalogs=[], + can_stream=False, + filter_encodings=[], + catalog=CatalogCapabilities(ddl=[SCHEMA_METADATA_ALTER]), + ) + with Client(server_path=WORKERS[sdk], worker_limit=1) as client: + attached = client.catalog_attach( + name="contents_reval", data_version_spec=None, implementation_version=None, client_capabilities=offer + ) + token = attached.attach_opaque_data + assert ( + attached.selected_capabilities is None + or SCHEMA_METADATA_ALTER not in attached.selected_capabilities.names() + ) + target: CatalogSchemaArgs = dict(attach_opaque_data=token, path=["main"]) + before = client.schema_get(**target) + version = client.catalog_version(attach_opaque_data=token) + with pytest.raises( + Exception, + match="(?i)not implemented|unimplemented|not supported|does not support|requires.*vgi.schema.metadata", + ): + client.schema_alter_metadata( + **target, changes=CatalogMetadataChanges(set_comment=True, comment="bad", set_tags={"a": "bad"}) + ) + assert client.schema_get(**target) == before + assert client.catalog_version(attach_opaque_data=token) == version + + +@pytest.mark.parametrize("sdk", sorted(WORKERS)) +def test_unqualified_provider_rejects_dependency_discovery(sdk: str) -> None: + offer = ClientCapabilities( + engine="reference-client", + native_formats=[], + catalogs=[], + can_stream=False, + filter_encodings=[], + catalog=CatalogCapabilities(read=[CATALOG_DEPENDENCIES_READ]), + ) + with Client(server_path=WORKERS[sdk], worker_limit=1) as client: + attached = client.catalog_attach( + name="contents_reval", data_version_spec=None, implementation_version=None, client_capabilities=offer + ) + assert ( + attached.selected_capabilities is None + or CATALOG_DEPENDENCIES_READ not in attached.selected_capabilities.names() + ) + token = attached.attach_opaque_data + version = client.catalog_version(attach_opaque_data=token) + with pytest.raises( + Exception, + match="(?i)not implemented|unimplemented|not supported|does not support|requires.*vgi.catalog.dependencies", + ): + client.catalog_dependencies(attach_opaque_data=token) + assert client.catalog_version(attach_opaque_data=token) == version diff --git a/tests/catalog/test_view_metadata_protocol.py b/tests/catalog/test_view_metadata_protocol.py new file mode 100644 index 00000000..481cfdd2 --- /dev/null +++ b/tests/catalog/test_view_metadata_protocol.py @@ -0,0 +1,86 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Creation metadata and comment mutations through the public protocol.""" + +from __future__ import annotations + +from dataclasses import replace + +import pytest + +from vgi.catalog import OnConflict, SchemaObjectType +from vgi.catalog.catalog_interface import AttachOpaqueData + +from .test_view_security_protocol import attach, backend, client # noqa: F401 +from .typing_helpers import CatalogColumnArgs, CatalogObjectArgs, present + +pytestmark = pytest.mark.usefixtures("backend") + + +@pytest.mark.parametrize("security", ["regular", "barrier"]) +def test_creation_and_comments_survive_reconnect_commit_and_rollback(security: str) -> None: + with client() as c: + token = attach(c) + target: CatalogObjectArgs = dict(attach_opaque_data=token, schema_path=["main"], name="v") + c.view_create( + **target, + definition="SELECT 1 AS x, 2 AS y", + aliases=["a.b", 'with"quote'], + security=security, + comment="", + tags={"team": "one", "empty": ""}, + column_comments={"a.b": "", 'with"quote': "kept"}, + ) + original = c.view_get(**target) + assert present(original).comment == "" and present(original).tags == {"team": "one", "empty": ""} + assert present(original).column_comments == {"a.b": "", 'with"quote': "kept"} + c.view_create(**target, definition="SELECT 9", on_conflict=OnConflict.IGNORE) + assert c.view_get(**target) == original + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.view_column_comment_set(**target, column_path=["a.b"], comment="new", transaction_opaque_data=tx) + c.view_comment_set(**target, comment=None, transaction_opaque_data=tx) + changed = c.view_get(**target, transaction_opaque_data=tx) + assert changed == replace(present(original), comment=None, column_comments={"a.b": "new", 'with"quote': "kept"}) + assert c.view_get(**target) == original + with client() as c: + c.catalog_transaction_rollback(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + assert c.view_get(**target) == original + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.view_column_comment_set(**target, column_path=["a.b"], comment=None, transaction_opaque_data=tx) + c.view_column_comment_set(**target, column_path=['with"quote'], comment="", transaction_opaque_data=tx) + c.catalog_transaction_commit(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + with client() as c: + current = c.view_get(**target) + assert current == replace(present(original), column_comments={'with"quote': ""}) + assert c.schema_contents(attach_opaque_data=token, path=["main"], type=SchemaObjectType.VIEW) == [current] + c.view_create(**target, definition="SELECT 3 AS z", on_conflict=OnConflict.REPLACE) + replaced = c.view_get(**target) + assert ( + present(replaced).comment is None + and present(replaced).tags == {} + and present(replaced).column_comments == {} + ) + assert present(replaced).aliases == [] and present(replaced).security == "regular" + + +@pytest.mark.parametrize("path", [[], ["a", "b"], [""], ["a\0b"]]) +def test_invalid_view_column_path_rejects_before_mutation(path: list[str]) -> None: + with client() as c: + token = attach(c) + target: CatalogObjectArgs = dict(attach_opaque_data=token, schema_path=["main"], name="v") + c.view_create(**target, definition="SELECT 1", aliases=["a.b"], comment="kept") + original = c.view_get(**target) + with pytest.raises(Exception, match="identifier|column path|component|empty|NUL"): + c.view_column_comment_set(**target, column_path=path, comment="bad", ignore_not_found=True) + assert c.view_get(**target) == original + + +def test_missing_view_comment_mutation_obeys_ignore_not_found() -> None: + with client() as c: + token = attach(c) + target: CatalogColumnArgs = dict( + attach_opaque_data=token, schema_path=["main"], name="absent", column_path=["a.b"] + ) + c.view_column_comment_set(**target, comment="unused", ignore_not_found=True) + with pytest.raises(Exception, match="does not exist"): + c.view_column_comment_set(**target, comment="unused") diff --git a/tests/catalog/test_view_security_protocol.py b/tests/catalog/test_view_security_protocol.py new file mode 100644 index 00000000..26cdeb19 --- /dev/null +++ b/tests/catalog/test_view_security_protocol.py @@ -0,0 +1,223 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 +"""Barrier metadata must survive RPC/persistence and never reach unaware readers.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path +from typing import Any + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient + +from vgi.catalog import OnConflict, SchemaObjectType, ViewInfo +from vgi.catalog.capabilities import TRANSACTION_DDL, VIEW_BARRIER_CREATE, VIEW_BARRIER_READ, CatalogCapabilities +from vgi.catalog.catalog_interface import AttachOpaqueData +from vgi.client import Client +from vgi.exceptions import CatalogCapabilityError +from vgi.protocol import CatalogAttachRequest, ClientCapabilities +from vgi.serve import create_app + +from .typing_helpers import CatalogObjectArgs, present +from .view_security_reference import PROFILES, ViewSecurityCatalog, ViewSecurityWorker + +KEY = b"view-security-reference-signing-key" +OFFER = ClientCapabilities( + engine="metadata-reference", native_formats=[], catalogs=[], can_stream=False, filter_encodings=[], catalog=PROFILES +) + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> ViewSecurityWorker: + monkeypatch.setattr(ViewSecurityCatalog, "database_path", str(tmp_path / "views.sqlite"), raising=False) + worker = ViewSecurityWorker(quiet=True) + worker._signing_key = KEY + return worker + + +def request(offer: ClientCapabilities | None = OFFER) -> CatalogAttachRequest: + return CatalogAttachRequest( + name="options", data_version_spec=None, implementation_version=None, client_capabilities=offer + ) + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(create_app(ViewSecurityWorker, describe=False, signing_key=KEY)) + ) + + +def attach(c: Client) -> AttachOpaqueData: + return c.catalog_attach( + name="options", data_version_spec=None, implementation_version=None, client_capabilities=OFFER + ).attach_opaque_data + + +def create(worker: ViewSecurityWorker, token: bytes, **kwargs: Any) -> None: + worker.catalog_view_create( + token, ["main"], "v", "SELECT 1 AS x", kwargs.pop("on_conflict", OnConflict.ERROR), aliases=["a.b"], **kwargs + ) + + +def test_public_rpc_transactions_reconnect_and_metadata_mutations(backend: ViewSecurityWorker) -> None: + with client() as c: + token = attach(c) + target: CatalogObjectArgs = dict(attach_opaque_data=token, schema_path=["main"], name="v") + c.view_create(**target, definition="SELECT 1 AS x", aliases=["a.b"], security="barrier") + first = c.view_get(**target) + assert present(first).security == "barrier" and present(first).aliases == ["a.b"] + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.view_create( + **target, + definition="SELECT 2 AS x", + security="regular", + on_conflict=OnConflict.REPLACE, + transaction_opaque_data=tx, + ) + assert present(c.view_get(**target, transaction_opaque_data=tx)).security == "regular" + assert present(c.view_get(**target)).security == "barrier" + with client() as c: + c.catalog_transaction_rollback(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + assert c.view_get(**target) == first + tx = c.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(token)) + c.view_rename(**target, new_name="renamed", transaction_opaque_data=tx) + target["name"] = "renamed" + c.view_comment_set(**target, comment="kept", transaction_opaque_data=tx) + with client() as c: + c.catalog_transaction_commit(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + c.catalog_transaction_commit(attach_opaque_data=AttachOpaqueData(token), transaction_opaque_data=present(tx)) + info = c.view_get(**target) + assert ( + present(info).security == "barrier" and present(info).aliases == ["a.b"] and present(info).comment == "kept" + ) + assert c.schema_contents(attach_opaque_data=token, path=["main"], type=SchemaObjectType.VIEW)[0] == info + + +@pytest.mark.parametrize( + "offer", + [ + None, + replace(OFFER, catalog=CatalogCapabilities()), + replace(OFFER, catalog=CatalogCapabilities(ddl=[VIEW_BARRIER_CREATE])), + ], +) +def test_unnegotiated_create_and_replace_have_no_effect( + backend: ViewSecurityWorker, offer: ClientCapabilities | None +) -> None: + full = backend.catalog_attach(request()).attach_opaque_data + token = backend.catalog_attach(request(offer)).attach_opaque_data + create(backend, full, security="barrier") + original = backend.catalog_view_get(full, ["main"], "v").to_optional() + before = backend.catalog_version(full).version + for security in ("regular", "barrier"): + with pytest.raises(CatalogCapabilityError): + create(backend, token, security=security, on_conflict=OnConflict.REPLACE) + with pytest.raises(CatalogCapabilityError): + create(backend, token, security="barrier", on_conflict=OnConflict.IGNORE) + assert backend.catalog_version(full).version == before + assert backend.catalog_view_get(full, ["main"], "v").to_optional() == original + + +@pytest.mark.parametrize("operation", ["get", "listing", "contents", "rename", "comment", "drop"]) +def test_incapable_and_withdrawn_readers_reject_including_cached_revalidation( + backend: ViewSecurityWorker, + monkeypatch: pytest.MonkeyPatch, + operation: str, +) -> None: + full = backend.catalog_attach(request()).attach_opaque_data + empty = backend.catalog_attach(request(None)).attach_opaque_data + create(backend, full, security="barrier") + snapshot = backend.catalog_contents(full) + assert backend.catalog_contents(full, snapshot.etag).not_modified + + def read(token: bytes) -> Any: + if operation == "get": + return backend.catalog_view_get(token, ["main"], "v") + if operation == "listing": + return backend.catalog_schema_contents_views(token, ["main"]) + if operation == "contents": + return backend.catalog_contents(token, snapshot.etag) + if operation == "rename": + return backend.catalog_view_rename(token, ["main"], "v", "new") + if operation == "comment": + return backend.catalog_view_comment_set(token, ["main"], "v", "new") + return backend.catalog_view_drop(token, ["main"], "v") + + with pytest.raises(CatalogCapabilityError, match=VIEW_BARRIER_READ): + read(empty) + monkeypatch.setattr(ViewSecurityCatalog, "profiles", CatalogCapabilities()) + with pytest.raises(CatalogCapabilityError, match=VIEW_BARRIER_READ): + read(full) + + +def test_barrier_change_invalidates_etag_even_with_constant_provider_token(backend: ViewSecurityWorker) -> None: + token = backend.catalog_attach(request()).attach_opaque_data + create(backend, token) + regular = backend.catalog_contents(token) + create(backend, token, security="barrier", on_conflict=OnConflict.REPLACE) + barrier = backend.catalog_contents(token, regular.etag) + assert not barrier.not_modified and barrier.etag != regular.etag + from vgi.catalog.contents_codec import decode_collection + + assert decode_collection(ViewInfo, barrier.views)[0].security == "barrier" + + +def test_transactional_barrier_create_requires_ddl_guarantee(backend: ViewSecurityWorker) -> None: + offer = replace(OFFER, catalog=replace(PROFILES, transaction=[])) + token = backend.catalog_attach(request(offer)).attach_opaque_data + tx = backend.catalog_transaction_begin(token).transaction_opaque_data + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + create(backend, token, security="barrier", transaction_opaque_data=tx) + assert backend.catalog_view_get(token, ["main"], "v", tx).to_optional() is None + backend.catalog_transaction_rollback(token, present(tx)) + + +@pytest.mark.parametrize("operation", ["rename", "comment", "drop"]) +def test_transactional_barrier_mutation_requires_ddl_guarantee(backend: ViewSecurityWorker, operation: str) -> None: + full = backend.catalog_attach(request()).attach_opaque_data + create(backend, full, security="barrier") + token = backend.catalog_attach( + request(replace(OFFER, catalog=replace(PROFILES, transaction=[]))) + ).attach_opaque_data + tx = backend.catalog_transaction_begin(token).transaction_opaque_data + with pytest.raises(CatalogCapabilityError, match=TRANSACTION_DDL): + if operation == "rename": + backend.catalog_view_rename(token, ["main"], "v", "new", transaction_opaque_data=tx) + elif operation == "comment": + backend.catalog_view_comment_set(token, ["main"], "v", "new", transaction_opaque_data=tx) + else: + backend.catalog_view_drop(token, ["main"], "v", transaction_opaque_data=tx) + assert backend.catalog_view_get(token, ["main"], "v", tx).to_optional().security == "barrier" + backend.catalog_transaction_rollback(token, present(tx)) + + +@pytest.mark.parametrize("security", [None, "", "secure", "BARRIER", "barrier\0", 1]) +def test_unknown_modes_reject_before_provider(backend: ViewSecurityWorker, security: Any) -> None: + token = backend.catalog_attach(request()).attach_opaque_data + before = backend.catalog_version(token).version + with pytest.raises(ValueError, match="security"): + create(backend, token, security=security) + assert backend.catalog_version(token).version == before + + +def test_missing_wire_security_never_defaults_to_regular() -> None: + info = ViewInfo(name="v", schema_path=["main"], definition="SELECT 1", comment=None, tags={}, security="barrier") + batch = pa.ipc.open_stream(info.serialize_to_bytes()).read_next_batch() + index = batch.schema.get_field_index("security") + invalid = [ + batch.remove_column(index), + batch.set_column(index, batch.schema.field(index), pa.array([None], type=pa.string())), + ] + for wire in invalid: + with pytest.raises(ValueError, match="security"): + ViewInfo.deserialize_from_batch(wire) + + +def test_create_profile_requires_read_and_unknown_profile_versions_are_not_equivalent() -> None: + assert PROFILES.select(CatalogCapabilities(ddl=[VIEW_BARRIER_CREATE])) == CatalogCapabilities() + assert ( + PROFILES.select(CatalogCapabilities(read=["vgi.view.barrier.read.v2"], ddl=[VIEW_BARRIER_CREATE])) + == CatalogCapabilities() + ) diff --git a/tests/catalog/test_writable_signed_transactions.py b/tests/catalog/test_writable_signed_transactions.py new file mode 100644 index 00000000..b7af1c9d --- /dev/null +++ b/tests/catalog/test_writable_signed_transactions.py @@ -0,0 +1,288 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103, D106, D107 +"""Signed HTTP token plumbing through the actual writable fixture handlers. + +The embedded DuckDB transactor needs a private subcursor-capable build. This +test substitutes a small real SQLite transaction service at that transport +boundary; it does not qualify the unavailable DuckDB backend. Worker sealing, +public Client calls, HTTP continuation, generic write binding/processing and +catalog transaction callbacks remain the actual implementations. +""" + +from __future__ import annotations + +import sqlite3 +import uuid +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Any + +import pyarrow as pa +import pytest +from vgi_rpc import AnnotatedBatch +from vgi_rpc.http import make_wsgi_app +from vgi_rpc.http._testing import _SyncTestClient +from vgi_rpc.utils import serialize_record_batch_bytes + +from vgi._test_fixtures.writable.generic import GenericTableInsert +from vgi._test_fixtures.writable.table import transactor_proxy +from vgi._test_fixtures.writable.worker import WritableWorker +from vgi.arguments import Arguments +from vgi.catalog.catalog_interface import AttachOpaqueData +from vgi.client import Client +from vgi.client.catalog_mixin import CatalogClientError +from vgi.client.errors import ClientError +from vgi.invocation import GlobalInitResponse +from vgi.meta_worker import MetaWorker +from vgi.protocol import VgiCallState +from vgi.rpc_server import build_rpc_server +from vgi.serve import create_app + +_KEY = b"writable-signed-transaction-test-key" +_ROW_FIELDS: list[pa.Field[Any]] = [pa.field("id", pa.int64()), pa.field("name", pa.string())] +_ROWS = pa.schema(_ROW_FIELDS) +_COUNT = pa.schema([pa.field("count", pa.int64(), nullable=False)]) + + +class SQLiteTransactorService: + """Real transactional storage, limited to the transactor calls under test.""" + + def __init__(self, root: Path) -> None: + self.root = root + self.paths: dict[bytes, Path] = {} + self.transactions: dict[bytes, tuple[bytes, sqlite3.Connection]] = {} + self.calls: list[tuple[str, bytes, bytes]] = [] + + def register(self, *, attach_opaque_data: bytes, **kwargs: Any) -> None: + del kwargs + path = self.root / f"{attach_opaque_data.hex()}.sqlite" + with sqlite3.connect(path) as db: + db.execute("CREATE TABLE items (id INTEGER, name TEXT)") + self.paths[attach_opaque_data] = path + + def begin(self, *, attach_opaque_data: bytes) -> bytes: + token = uuid.uuid4().bytes + db = sqlite3.connect(self.paths[attach_opaque_data], isolation_level=None, check_same_thread=False) + db.execute("BEGIN") + self.transactions[token] = (attach_opaque_data, db) + self.calls.append(("begin", attach_opaque_data, token)) + return token + + def _connection(self, attach: bytes, token: bytes) -> sqlite3.Connection: + if token not in self.transactions: + raise ValueError("test transaction service received an unknown plaintext transaction token") + owner, db = self.transactions[token] + if owner != attach: + raise ValueError("test transaction service received a transaction for another attachment") + return db + + def commit(self, *, attach_opaque_data: bytes, tx_id: bytes) -> None: + db = self._connection(attach_opaque_data, tx_id) + db.commit() + db.close() + del self.transactions[tx_id] + self.calls.append(("commit", attach_opaque_data, tx_id)) + + def rollback(self, *, attach_opaque_data: bytes, tx_id: bytes) -> None: + db = self._connection(attach_opaque_data, tx_id) + db.rollback() + db.close() + del self.transactions[tx_id] + self.calls.append(("rollback", attach_opaque_data, tx_id)) + + def catalog_version(self, **kwargs: Any) -> int: + del kwargs + return 1 + + def table_schema(self, *, attach_opaque_data: bytes, tx_id: bytes, table_name: str) -> bytes: + assert table_name == "items" + self._connection(attach_opaque_data, tx_id) + self.calls.append(("table_schema", attach_opaque_data, tx_id)) + return _ROWS.serialize().to_pybytes() + + @contextmanager + def insert(self, *, attach_opaque_data: bytes, tx_id: bytes, table_name: str, result_mode: str) -> Iterator[Any]: + assert table_name == "items" + db = self._connection(attach_opaque_data, tx_id) + service = self + + class Exchange: + def exchange(self, input: AnnotatedBatch) -> AnnotatedBatch: + rows = input.batch.to_pylist() + db.executemany("INSERT INTO items VALUES (?, ?)", [(row["id"], row["name"]) for row in rows]) + service.calls.append(("insert", attach_opaque_data, tx_id)) + result = ( + input.batch + if result_mode == "rows" + else pa.RecordBatch.from_pydict({"count": [len(rows)]}, schema=_COUNT) + ) + return AnnotatedBatch(batch=result) + + yield Exchange() + + def committed_rows(self, attach: bytes) -> list[tuple[int, str]]: + with sqlite3.connect(self.paths[attach]) as db: + return db.execute("SELECT id, name FROM items ORDER BY id").fetchall() + + def close(self) -> None: + for _, db in self.transactions.values(): + db.close() + + +@pytest.fixture +def service(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[SQLiteTransactorService]: + backend = SQLiteTransactorService(tmp_path) + monkeypatch.setattr(transactor_proxy, "_get_proxy", lambda: backend) + yield backend + backend.close() + + +def _client(app: Any, attach: bytes | None = None) -> Client: + return Client.from_http( + "http://vgi.test", httpx_client=_SyncTestClient(app), attach_opaque_data=attach, worker_limit=1 + ) + + +def _attach(client: Client) -> bytes: + result = client.catalog_attach(name="writable", data_version_spec=None, implementation_version=None) + assert result.attach_opaque_data is not None + return result.attach_opaque_data + + +def _write(client: Client, tx: bytes, mode: str, start: int = 1, *, has_finalize: bool = False) -> list[dict[str, Any]]: + options = pa.RecordBatch.from_pydict({"result_mode": [mode]}) + batches = [ + pa.RecordBatch.from_pylist([{"id": value, "name": f"row-{value}"}], schema=_ROWS) + for value in (start, start + 1) + ] + result = client.table_in_out_function( + function_name="generic_writable_insert", + schema_path=["main"], + input=iter(batches), + arguments=Arguments( + positional=(pa.scalar("items"),), + named={"write_options": pa.scalar(serialize_record_batch_bytes(options))}, + ), + transaction_opaque_data=tx, + has_finalize=has_finalize, + ) + return [row for batch in result for row in batch.to_pylist()] + + +@pytest.mark.parametrize("mode", ["count", "rows"]) +def test_signed_http_write_commit_and_rollback(service: SQLiteTransactorService, mode: str) -> None: + app = create_app(WritableWorker, describe=False, signing_key=_KEY) + with _client(app) as catalog: + sealed_attach = _attach(catalog) + owner = next(iter(service.paths)) + with _client(app, sealed_attach) as writer: + for operation, start in [("commit", 1), ("rollback", 3)]: + sealed_tx = catalog.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(sealed_attach)) + assert sealed_tx is not None + plaintext = service.calls[-1][2] + assert sealed_tx != plaintext + rows = _write(writer, sealed_tx, mode, start) + expected = ( + [{"count": 1}, {"count": 1}] + if mode == "count" + else [{"id": value, "name": f"row-{value}"} for value in (start, start + 1)] + ) + assert rows == expected + assert service.committed_rows(owner) == ([] if operation == "commit" else [(1, "row-1"), (2, "row-2")]) + getattr(catalog, f"catalog_transaction_{operation}")( + attach_opaque_data=sealed_attach, transaction_opaque_data=sealed_tx + ) + assert (operation, owner, plaintext) in service.calls + assert service.calls.count(("insert", owner, plaintext)) == 2 + assert not any(token == sealed_tx for _, _, token in service.calls) + assert service.committed_rows(owner) == [(1, "row-1"), (2, "row-2")] + assert not service.transactions + + +@pytest.mark.parametrize("mode", ["count", "rows"]) +def test_signed_http_wrong_attachment_rejected_before_service(service: SQLiteTransactorService, mode: str) -> None: + app = create_app(WritableWorker, describe=False, signing_key=_KEY) + with _client(app) as catalog: + owner = _attach(catalog) + other = _attach(catalog) + tx = catalog.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(owner)) + assert tx is not None + before = list(service.calls) + with _client(app, other) as writer, pytest.raises(ClientError, match="transaction_opaque_data"): + _write(writer, tx, mode) + assert service.calls == before + for attach in service.paths: + assert service.committed_rows(attach) == [] + for operation in (catalog.catalog_transaction_commit, catalog.catalog_transaction_rollback): + with pytest.raises(CatalogClientError, match="transaction_opaque_data"): + operation(attach_opaque_data=AttachOpaqueData(other), transaction_opaque_data=tx) + catalog.catalog_transaction_rollback(attach_opaque_data=AttachOpaqueData(owner), transaction_opaque_data=tx) + assert not service.transactions + + +@pytest.mark.parametrize("use_meta_worker", [False, True]) +def test_signed_http_callback_token_survives_init_continuation_and_finalize( + service: SQLiteTransactorService, monkeypatch: pytest.MonkeyPatch, use_meta_worker: bool +) -> None: + """All callbacks see provider bytes, while serialized call state keeps the seal.""" + seen: list[tuple[str, bytes | None]] = [] + original_bind = GenericTableInsert.on_bind + original_process = GenericTableInsert.process + plaintext: bytes | None = None + sealed: bytes | None = None + + def check(phase: str, params: Any) -> None: + seen.append((phase, params.transaction_opaque_data)) + bind = params.bind_call if phase == "bind" else params.init_call.bind_call + assert bind.transaction_opaque_data == sealed + assert params.transaction_opaque_data == plaintext + if phase in {"process", "finalize"}: + state = VgiCallState(init_call=params.init_call, init_response=params.init_response) + raw = state.serialize_to_bytes() + assert plaintext is not None and sealed is not None + assert plaintext not in raw + assert sealed in raw + + def on_bind(cls: Any, params: Any) -> Any: + check("bind", params) + return original_bind(params) + + def on_init(cls: Any, params: Any) -> GlobalInitResponse: + check("init", params) + return GlobalInitResponse() + + def process(cls: Any, params: Any, state: None, batch: pa.RecordBatch, out: Any) -> None: + check("process", params) + original_process(params, state, batch, out) + + def finalize(cls: Any, params: Any) -> list[pa.RecordBatch]: + check("finalize", params) + return [] + + monkeypatch.setattr(GenericTableInsert, "on_bind", classmethod(on_bind)) + monkeypatch.setattr(GenericTableInsert, "on_init", classmethod(on_init)) + monkeypatch.setattr(GenericTableInsert, "process", classmethod(process)) + monkeypatch.setattr(GenericTableInsert, "finalize", classmethod(finalize)) + if use_meta_worker: + meta = MetaWorker([WritableWorker(quiet=True)]) + meta._signing_key = _KEY + app = make_wsgi_app(build_rpc_server(meta, transport="http", describe=False), token_key=_KEY) + else: + app = create_app(WritableWorker, describe=False, signing_key=_KEY) + with _client(app) as catalog: + attach = _attach(catalog) + sealed = catalog.catalog_transaction_begin(attach_opaque_data=AttachOpaqueData(attach)) + assert sealed is not None + plaintext = service.calls[-1][2] + with _client(app, attach) as writer: + assert _write(writer, sealed, "count", has_finalize=True) == [{"count": 1}, {"count": 1}] + catalog.catalog_transaction_rollback( + attach_opaque_data=AttachOpaqueData(attach), transaction_opaque_data=sealed + ) + phases = [phase for phase, _ in seen] + assert phases.count("bind") >= 1 + assert phases.count("init") >= 1 + assert phases.count("process") == 2 + assert phases.count("finalize") == 1 + assert all(token == plaintext for _, token in seen) diff --git a/tests/catalog/transaction_begin_probe.py b/tests/catalog/transaction_begin_probe.py new file mode 100644 index 00000000..69fc6b6d --- /dev/null +++ b/tests/catalog/transaction_begin_probe.py @@ -0,0 +1,58 @@ +# Copyright 2026 Query Farm LLC - https://query.farm + +"""Native BEGIN retry probe: allocate a token, then lose its first response. + +Uses the existing diagnostic catalog's persistent active-token files, not a +transaction recovery service. The first BEGIN process exits after allocation +when VGI_TRANSACTION_BEGIN_PROBE_DROP_REPLY=1. A retry would allocate a second +token, leaving the first unknown to the client. No user data is written. +""" + +from __future__ import annotations + +import os + +from tests.catalog.transaction_cleanup_probe import ( + TransactionCleanupCatalog, + TransactionCleanupWorker, + _event, + _locked, + _root, +) +from vgi.catalog import AttachOpaqueData, TransactionOpaqueData + + +class BeginReplyProbeCatalog(TransactionCleanupCatalog): + """Lose one BEGIN response after the provider has allocated its transaction.""" + + def catalog_transaction_begin(self, *, attach_opaque_data: AttachOpaqueData) -> TransactionOpaqueData: + """Persist allocation, then exit once before returning its opaque token.""" + token = super().catalog_transaction_begin(attach_opaque_data=attach_opaque_data) + lose_response = False + with _locked() as root: + marker = root / "begin-response-lost" + if os.environ.get("VGI_TRANSACTION_BEGIN_PROBE_DROP_REPLY") == "1" and not marker.exists(): + marker.write_text(token.hex(), encoding="utf-8") + _event(root, "begin", "response_lost", attach_opaque_data, token) + lose_response = True + if lose_response: + os._exit(75) + return token + + +class BeginReplyProbeWorker(TransactionCleanupWorker): + """Reuse the ordinary diagnostic catalog and native subprocess dispatch.""" + + catalog_interface = BeginReplyProbeCatalog + + +def main() -> None: + """Keep each reproduction's allocation and framework state isolated.""" + root = _root() + os.environ["VGI_WORKER_SHARED_STORAGE"] = "sqlite" + os.environ["VGI_WORKER_SQLITE_PATH"] = str(root / "framework.sqlite") + BeginReplyProbeWorker.main() + + +if __name__ == "__main__": + main() diff --git a/tests/catalog/transaction_cleanup_probe.py b/tests/catalog/transaction_cleanup_probe.py new file mode 100644 index 00000000..1a7e7b95 --- /dev/null +++ b/tests/catalog/transaction_cleanup_probe.py @@ -0,0 +1,303 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D106 +"""Opt-in transaction lifetime probe; no data atomicity or recovery guarantees. + +Run ``python -m tests.catalog.transaction_cleanup_probe`` with required +``VGI_TRANSACTION_CLEANUP_PROBE_DIR=/absolute/isolated/directory``. Attach +catalog ``transaction_cleanup_probe`` and query ``main.items``: one row with +``id=1, label='tx:'``. The catalog is unfrozen, and schema +and table comments include the transaction's token, so native metadata lives +in its transaction-local catalog entries. There are no user data mutations. + +The harness can atomically replace ``control.json`` with, for example, +``{"commit":"reject"}`` or ``{"rollback":"after_completion_error"}``. +Each operation consumes its own key once. ``reject`` raises before removing +the active transaction; ``after_completion_error`` removes it, then raises +instead of returning a successful response. Omitted keys complete normally. + +``active-.json`` files contain only currently open transactions and +their schema comments. There is no completed-transaction or recovery ledger. +``events.jsonl`` records event, phase, token, attach, PID and optional mode. +All files, including framework SQLite storage, stay in the configured root. +This diagnostic targets the unsigned subprocess transport used by the native +harness; it does not qualify HTTP transaction-token unwrapping. +""" + +from __future__ import annotations + +import fcntl +import json +import os +import uuid +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Any + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import OutputCollector + +from vgi.catalog import ( + AttachOpaqueData, + Catalog, + CatalogAttachResult, + ReadOnlyCatalogInterface, + Schema, + SchemaInfo, + SchemaObjectType, + Table, + TableInfo, + TransactionOpaqueData, +) +from vgi.invocation import BindResponse, GlobalInitResponse +from vgi.table_function import BindParams, InitParams, ProcessParams, TableFunctionGenerator +from vgi.worker import Worker + +_ROW_FIELDS: list[pa.Field[Any]] = [pa.field("id", pa.int64()), pa.field("label", pa.string())] +_ROWS = pa.schema(_ROW_FIELDS) + + +def _root() -> Path: + value = os.environ.get("VGI_TRANSACTION_CLEANUP_PROBE_DIR") + if not value or not Path(value).is_absolute(): + raise ValueError("VGI_TRANSACTION_CLEANUP_PROBE_DIR must name an isolated absolute directory") + root = Path(value) + root.mkdir(parents=True, exist_ok=True) + return root + + +@contextmanager +def _locked() -> Iterator[Path]: + root = _root() + with (root / "state.lock").open("a") as lock: + fcntl.flock(lock.fileno(), fcntl.LOCK_EX) + try: + yield root + finally: + fcntl.flock(lock.fileno(), fcntl.LOCK_UN) + + +def _write_json(path: Path, value: Any) -> None: + temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") + temporary.write_text(json.dumps(value), encoding="utf-8") + temporary.replace(path) + + +def _event(root: Path, event: str, phase: str, attach: bytes, token: bytes | None, **extra: Any) -> None: + # Callers hold state.lock, including across event append and state changes. + with (root / "events.jsonl").open("a", encoding="utf-8") as log: + log.write( + json.dumps( + { + "event": event, + "phase": phase, + "attach": attach.hex(), + "token": token.hex() if token else None, + "pid": os.getpid(), + **extra, + } + ) + + "\n" + ) + + +def _active(root: Path, attach: bytes, token: bytes | None) -> dict[str, Any]: + if not token: + return {"schema_comment": "outside transaction"} + path = root / f"active-{token.hex()}.json" + if not path.exists(): + raise ValueError("transaction_cleanup_probe: transaction is not active") + data: dict[str, Any] = json.loads(path.read_text(encoding="utf-8")) + if data["attach"] != attach.hex(): + raise ValueError("transaction_cleanup_probe: transaction belongs to another attachment") + return data + + +def _comment(attach: bytes, token: bytes | None) -> str: + with _locked() as root: + return str(_active(root, attach, token)["schema_comment"]) + + +def _consume_mode(root: Path, operation: str) -> str: + path = root / "control.json" + controls = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} + mode = controls.pop(operation, "success") + if mode not in {"success", "reject", "after_completion_error"}: + raise ValueError(f"transaction_cleanup_probe: unknown failure mode {mode!r}") + _write_json(path, controls) + return str(mode) + + +@dataclass(kw_only=True) +class _ScanState(ArrowSerializableDataclass): + """One-row producer state that remains serializable across transport turns.""" + + emitted: bool = False + + +class TransactionCleanupScan(TableFunctionGenerator[None, _ScanState]): + """Read one diagnostic row only while the requested transaction is active.""" + + State = _ScanState + + class Meta: + name = "transaction_cleanup_scan" + max_workers = 1 + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + del params # Also used for schema-only catalog introspection. + return BindResponse(output_schema=_ROWS) + + @classmethod + def on_init(cls, params: InitParams[None]) -> GlobalInitResponse: + del params + return GlobalInitResponse(max_workers=1) + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> _ScanState: + del params + return _ScanState() + + @classmethod + def process(cls, params: ProcessParams[None], state: _ScanState, out: OutputCollector) -> None: + if state.emitted: + out.finish() + return + assert params.init_call is not None + attach = bytes(params.attach_opaque_data or b"") + token = params.init_call.bind_call.transaction_opaque_data + if not attach or not token: + raise ValueError("transaction_cleanup_probe: scan requires an active transaction") + with _locked() as root: + _active(root, attach, token) + _event(root, "scan", "read", attach, token) + out.emit(pa.RecordBatch.from_pydict({"id": [1], "label": [f"tx:{token.hex()}"]}, schema=_ROWS)) + state.emitted = True + + +CATALOG = Catalog( + name="transaction_cleanup_probe", + default_schema="main", + schemas=[ + Schema( + path=["main"], + functions=[TransactionCleanupScan], + tables=[Table(name="items", function=TransactionCleanupScan)], + ) + ], +) + + +class TransactionCleanupCatalog(ReadOnlyCatalogInterface): + """Transaction-local metadata with controllable completion callback failures.""" + + catalog = CATALOG + supports_transactions = True + catalog_version_frozen = False + + def catalog_attach(self, **kwargs: Any) -> CatalogAttachResult: + result = super().catalog_attach(**kwargs) + attach = AttachOpaqueData(uuid.uuid4().bytes) + with _locked() as root: + _event(root, "attach", "completed", attach, None) + return replace( + result, + attach_opaque_data=attach, + attach_opaque_data_required=True, + supports_transactions=True, + catalog_version_frozen=False, + catalog_version=1, + supports_catalog_contents=False, + ) + + def catalog_version( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + ctx: Any = None, + ) -> int: + del ctx + _comment(attach_opaque_data, transaction_opaque_data) + return 1 + + def catalog_transaction_begin(self, *, attach_opaque_data: AttachOpaqueData) -> TransactionOpaqueData: + token = TransactionOpaqueData(uuid.uuid4().bytes) + with _locked() as root: + _write_json( + root / f"active-{token.hex()}.json", + {"attach": attach_opaque_data.hex(), "schema_comment": f"tx:{token.hex()}"}, + ) + _event(root, "begin", "completed", attach_opaque_data, token) + return token + + def _finish(self, operation: str, attach: bytes, token: bytes) -> None: + with _locked() as root: + _event(root, operation, "attempt", attach, token) + try: + _active(root, attach, token) + except ValueError: + _event(root, operation, "rejected", attach, token, mode="inactive") + raise + mode = _consume_mode(root, operation) + if mode == "reject": + _event(root, operation, "rejected", attach, token, mode=mode) + raise RuntimeError(f"transaction_cleanup_probe: {operation} rejected before completion") + (root / f"active-{token.hex()}.json").unlink() + _event(root, operation, "completed", attach, token, mode=mode) + if mode == "after_completion_error": + _event(root, operation, "response_error", attach, token, mode=mode) + raise RuntimeError(f"transaction_cleanup_probe: {operation} response error after completion") + + def catalog_transaction_commit( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData + ) -> None: + self._finish("commit", attach_opaque_data, transaction_opaque_data) + + def catalog_transaction_rollback( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData + ) -> None: + self._finish("rollback", attach_opaque_data, transaction_opaque_data) + + def schemas(self, **kwargs: Any) -> list[SchemaInfo]: + comment = _comment(kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + return [replace(info, comment=comment) for info in super().schemas(**kwargs)] + + def schema_get(self, **kwargs: Any) -> SchemaInfo | None: + comment = _comment(kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + info = super().schema_get(**kwargs) + return replace(info, comment=comment) if info is not None else None + + def table_get(self, **kwargs: Any) -> TableInfo | None: + comment = _comment(kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + info = super().table_get(**kwargs) + return replace(info, comment=comment) if info is not None else None + + def schema_contents(self, **kwargs: Any) -> Any: + comment = _comment(kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + result = super().schema_contents(**kwargs) + if kwargs["type"] == SchemaObjectType.TABLE: + return [replace(info, comment=comment) for info in result] + return result + + +class TransactionCleanupWorker(Worker): + """Expose the probe through ordinary native subprocess RPC dispatch.""" + + catalog = CATALOG + catalog_interface = TransactionCleanupCatalog + + +def main() -> None: + """Confine active transactions, control, events and framework state to one root.""" + root = _root() + os.environ["VGI_WORKER_SHARED_STORAGE"] = "sqlite" + os.environ["VGI_WORKER_SQLITE_PATH"] = str(root / "framework.sqlite") + TransactionCleanupWorker.main() + + +if __name__ == "__main__": + main() diff --git a/tests/catalog/transactional_write_reference.py b/tests/catalog/transactional_write_reference.py new file mode 100644 index 00000000..32fcda19 --- /dev/null +++ b/tests/catalog/transactional_write_reference.py @@ -0,0 +1,613 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D103, D106 +"""Single-process SQLite authority for native transactional INSERT/CTAS tests. + +Set VGI_TRANSACTIONAL_WRITE_DIR to an isolated absolute directory and run +``python -m tests.catalog.transactional_write_reference --port 0``. The launcher +prints ``PORT:`` and serves signed HTTP on loopback. Its SQLite database +starts with ``items(id BIGINT, label VARCHAR)`` containing ``(-1, 'baseline')``. + +control.json selects one subsequent write: ``{"phase": "input", "table": +"fail_target", "after_batches": 2}``. Supported phases are init/input/finalize; +the optional table selector leaves other writes unaffected. A matching control +is consumed at execution init, so later control changes cannot alter an earlier +write. INPUT errors happen *after* inserting the selected batch into SQLite. +Neither an injected error nor stream cancellation rolls the transaction back: +the native client must invoke the catalog rollback method. Independent readers +of data.sqlite therefore observe actual committed state, not diagnostic files. + +This deliberately serializes all access in one HTTP worker process. It claims +no multi-process coordination, retries, crash recovery or concurrent isolation. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sqlite3 +import threading +import uuid +from collections.abc import Iterator +from contextlib import closing, contextmanager +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Any, ClassVar +from wsgiref.simple_server import make_server + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import OutputCollector + +from vgi._test_fixtures.simple_writable import _parse_write_options +from vgi.catalog import ( + AttachOpaqueData, + Catalog, + CatalogAttachResult, + OnConflict, + ReadOnlyCatalogInterface, + ScanFunctionResult, + Schema, + SchemaInfo, + SchemaObjectType, + TableInfo, + TransactionOpaqueData, +) +from vgi.catalog.catalog_interface import SerializedSchema +from vgi.invocation import BindResponse, GlobalInitResponse +from vgi.table_function import ( + BindParams, + InitParams, + ProcessParams, + TableFunctionGenerator, + TableInOutFunctionInitPhase, +) +from vgi.table_in_out_function import TableInOutGenerator +from vgi.worker import Worker +from vgi.write_results import write_result_schema + +from .typing_helpers import present + +_ROW_FIELDS: list[pa.Field[Any]] = [pa.field("id", pa.int64()), pa.field("label", pa.string())] +_ROWS = pa.schema(_ROW_FIELDS) +_COUNT = write_result_schema("count", _ROWS) +_PREFIX = "transactional_write_reference:" + + +def _error(message: str) -> RuntimeError: + return RuntimeError(f"{_PREFIX} {message}") + + +def _root() -> Path: + value = os.environ.get("VGI_TRANSACTIONAL_WRITE_DIR") + if not value or not Path(value).is_absolute(): + raise _error("VGI_TRANSACTIONAL_WRITE_DIR must name an isolated absolute directory") + root = Path(value) + root.mkdir(parents=True, exist_ok=True) + return root + + +def _identifier(name: str) -> str: + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) is None: + raise _error("this test provider accepts simple table names only") + return f'"{name}"' + + +def _table_argument(params: BindParams[None] | InitParams[None] | ProcessParams[None]) -> str: + bind = present(params).bind_call if isinstance(params, BindParams) else present(params.init_call).bind_call + arguments = bind.arguments.positional + if len(arguments) != 1 or arguments[0] is None: + raise _error("one table-name argument is required") + name = arguments[0].as_py() + if not isinstance(name, str): + raise _error("table name must be a string") + _identifier(name) + return name + + +def _execution_id(params: ProcessParams[None]) -> bytes: + if params.init_response is None: + raise _error("write callback has no execution id") + return params.init_response.execution_id + + +@dataclass +class _Transaction: + attach: bytes + connection: sqlite3.Connection + + +@dataclass +class _Execution: + attach: bytes + token: bytes + table: str + phase: str | None + after_batches: int + batches: int = 0 + rows: int = 0 + + +class _Authority: + """One process owns all SQLite connections; the lock serializes callbacks.""" + + lock: ClassVar[Any] = threading.RLock() + root: ClassVar[Path | None] = None + attachments: ClassVar[set[bytes]] = set() + transactions: ClassVar[dict[bytes, _Transaction]] = {} + executions: ClassVar[dict[bytes, _Execution]] = {} + + @classmethod + def initialize(cls) -> None: + with cls.lock: + root = _root() + if cls.root is not None: + if cls.root != root: + raise _error("storage directory cannot change within this worker process") + return + cls.root = root + with closing(cls.connect()) as db: + db.execute("PRAGMA journal_mode=WAL") + if db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='items'").fetchone() is None: + db.execute("BEGIN") + db.execute('CREATE TABLE "items" (id BIGINT, label VARCHAR)') + db.execute('INSERT INTO "items" VALUES (?, ?)', (-1, "baseline")) + db.execute("COMMIT") + + @classmethod + def connect(cls) -> sqlite3.Connection: + assert cls.root is not None + return sqlite3.connect(cls.root / "data.sqlite", isolation_level=None, check_same_thread=False, timeout=10) + + @classmethod + def event( + cls, + event: str, + *, + attach: bytes, + token: bytes | None = None, + execution_id: bytes | None = None, + phase: str = "completed", + **details: Any, + ) -> None: + assert cls.root is not None + with (cls.root / "events.jsonl").open("a", encoding="utf-8") as target: + target.write( + json.dumps( + { + "event": event, + "phase": phase, + "attach": attach.hex(), + "token": token.hex() if token is not None else None, + "execution_id": execution_id.hex() if execution_id is not None else None, + "pid": os.getpid(), + "active_transactions": len(cls.transactions), + **details, + } + ) + + "\n" + ) + + @classmethod + def transaction(cls, attach: bytes, token: bytes | None) -> _Transaction: + if attach not in cls.attachments: + raise _error("unknown attachment") + transaction = cls.transactions.get(token or b"") + if transaction is None or transaction.attach != attach: + raise _error("transaction is inactive or belongs to another attachment") + return transaction + + @classmethod + @contextmanager + def connection(cls, attach: bytes, token: bytes | None) -> Iterator[sqlite3.Connection]: + with cls.lock: + if attach not in cls.attachments: + raise _error("unknown attachment") + try: + if token is not None: + yield cls.transaction(attach, token).connection + else: + with closing(cls.connect()) as db: + yield db + except sqlite3.Error as exc: + # Do not rollback here: a native rollback RPC must undo the real changes. + raise _error(str(exc)) from exc + + @classmethod + def control(cls, table: str) -> tuple[str | None, int]: + assert cls.root is not None + path = cls.root / "control.json" + data = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} + if not data or data.get("table", table).casefold() != table.casefold(): + return None, 2 + phase = data.get("phase") + after_batches = data.get("after_batches", 2) + if phase not in {"init", "input", "finalize"}: + raise _error("control phase must be init, input or finalize") + if not isinstance(after_batches, int) or isinstance(after_batches, bool) or after_batches < 1: + raise _error("after_batches must be a positive integer") + temporary = path.with_name(f".control-{uuid.uuid4().hex}.tmp") + temporary.write_text("{}", encoding="utf-8") + temporary.replace(path) + return phase, after_batches + + @classmethod + def execution(cls, params: ProcessParams[None]) -> _Execution: + execution = cls.executions.get(_execution_id(params)) + if execution is None: + raise _error("execution was not initialized") + if execution.attach != params.attach_opaque_data or execution.token != params.transaction_opaque_data: + raise _error("execution belongs to another attachment or transaction") + cls.transaction(execution.attach, execution.token) + return execution + + @classmethod + def failure(cls, execution_id: bytes, execution: _Execution, phase: str) -> None: + cls.event( + "failure", + attach=execution.attach, + token=execution.token, + execution_id=execution_id, + phase=phase, + table=execution.table, + batch_index=execution.batches, + rows=execution.rows, + stored_rows=execution.rows, + ) + raise _error(f"{phase} injected failure after {execution.batches} batches and {execution.rows} stored rows") + + +@dataclass(kw_only=True) +class _ScanState(ArrowSerializableDataclass): + offset: int = 0 + + +class TransactionalScan(TableFunctionGenerator[None, _ScanState]): + """Read the real table from the transaction's connection.""" + + class Meta: + name = "transactional_write_scan" + max_workers = 1 + projection_pushdown = False + filter_pushdown = False + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + _table_argument(params) + return BindResponse(output_schema=_ROWS) + + @classmethod + def on_init(cls, params: InitParams[None]) -> GlobalInitResponse: + del params + return GlobalInitResponse(max_workers=1) + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> _ScanState: + del params + return _ScanState() + + @classmethod + def process(cls, params: ProcessParams[None], state: _ScanState, out: OutputCollector) -> None: + table = _table_argument(params) + attach = bytes(params.attach_opaque_data or b"") + token = params.transaction_opaque_data + with _Authority.connection(attach, token) as db: + rows = db.execute( + f"SELECT id, label FROM {_identifier(table)} ORDER BY rowid LIMIT 2048 OFFSET ?", (state.offset,) + ).fetchall() + _Authority.event("scan", attach=attach, token=token, table=table, rows=len(rows)) + if not rows: + out.finish() + return + state.offset += len(rows) + out.emit( + pa.RecordBatch.from_pydict( + {"id": [row[0] for row in rows], "label": [row[1] for row in rows]}, schema=_ROWS + ) + ) + + +class TransactionalInsert(TableInOutGenerator[None, None]): + """Insert into the open SQLite transaction without committing or rolling back.""" + + class Meta: + name = "transactional_write_insert" + max_workers = 1 + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + _table_argument(params) + options = _parse_write_options(params) + if options["result_mode"] not in {"count", "rows"} or options["on_conflict"] != "throw": + raise _error("only count/rows INSERT with ordinary conflict behavior is supported") + return BindResponse(output_schema=write_result_schema(options["result_mode"], _ROWS)) + + @classmethod + def on_init(cls, params: InitParams[None]) -> GlobalInitResponse: + table = _table_argument(params) + attach = bytes(params.attach_opaque_data or b"") + token = params.transaction_opaque_data + with _Authority.lock: + _Authority.transaction(attach, token) + assert token is not None + execution = _Authority.executions.get(params.execution_id) + if execution is None: + phase, after_batches = _Authority.control(table) + execution = _Execution(attach, token, table, phase, after_batches) + _Authority.executions[params.execution_id] = execution + elif (execution.attach, execution.token, execution.table) != (attach, token, table): + raise _error("execution id was reused for another write") + finalize = params.init_call.phase == TableInOutFunctionInitPhase.FINALIZE + _Authority.event( + "init", + attach=attach, + token=token, + execution_id=params.execution_id, + phase="finalize" if finalize else "input", + table=table, + ) + if execution.phase == "init" and not finalize: + _Authority.failure(params.execution_id, execution, "init") + return GlobalInitResponse(max_workers=1) + + @classmethod + def process(cls, params: ProcessParams[None], state: None, batch: pa.RecordBatch, out: OutputCollector) -> None: + del state + with _Authority.lock: + execution = _Authority.execution(params) + rows = list(zip(batch.column("id").to_pylist(), batch.column("label").to_pylist(), strict=True)) + with _Authority.connection(execution.attach, execution.token) as db: + db.executemany(f"INSERT INTO {_identifier(execution.table)} (id, label) VALUES (?, ?)", rows) + execution.batches += 1 + execution.rows += len(rows) + _Authority.event( + "input", + attach=execution.attach, + token=execution.token, + execution_id=_execution_id(params), + table=execution.table, + batch_index=execution.batches, + batch_rows=len(rows), + rows=len(rows), + stored_rows=execution.rows, + ) + if execution.phase == "input" and execution.batches >= execution.after_batches: + _Authority.failure(_execution_id(params), execution, "input") + if _parse_write_options(params)["result_mode"] == "rows": + out.emit( + pa.RecordBatch.from_pydict( + {"id": [row[0] for row in rows], "label": [row[1] for row in rows]}, schema=_ROWS + ) + ) + else: + out.emit(pa.RecordBatch.from_pydict({"count": [len(rows)]}, schema=_COUNT)) + + @classmethod + def finalize(cls, params: ProcessParams[None]) -> list[pa.RecordBatch]: + with _Authority.lock: + execution = _Authority.execution(params) + _Authority.event( + "finalize", + attach=execution.attach, + token=execution.token, + execution_id=_execution_id(params), + table=execution.table, + batches=execution.batches, + rows=execution.rows, + ) + if execution.phase == "finalize": + _Authority.failure(_execution_id(params), execution, "finalize") + if _parse_write_options(params)["result_mode"] == "rows": + return [] + return [pa.RecordBatch.from_pydict({"count": [0]}, schema=_COUNT)] + + +CATALOG = Catalog( + name="transactional_writes", + default_schema="main", + schemas=[Schema(path=["main"], functions=[TransactionalScan, TransactionalInsert], tables=[])], +) + + +class TransactionalWriteCatalog(ReadOnlyCatalogInterface): + """Dynamic SQLite tables with genuine transactional DDL and row changes.""" + + catalog = CATALOG + supports_transactions = True + catalog_version_frozen = False + catalog_contents_attach_independent = False + + def catalog_attach(self, **kwargs: Any) -> CatalogAttachResult: + _Authority.initialize() + result = super().catalog_attach(**kwargs) + attach = AttachOpaqueData(uuid.uuid4().bytes) + with _Authority.lock: + _Authority.attachments.add(attach) + _Authority.event("attach", attach=attach) + return replace( + result, + attach_opaque_data=attach, + attach_opaque_data_required=True, + supports_transactions=True, + supports_catalog_contents=False, + catalog_version_frozen=False, + catalog_version=self.catalog_version(attach_opaque_data=attach, transaction_opaque_data=None), + ) + + def catalog_version( + self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes | None = None, **kwargs: Any + ) -> int: + with _Authority.connection(attach_opaque_data, transaction_opaque_data) as db: + return int(db.execute("PRAGMA schema_version").fetchone()[0]) + + def catalog_transaction_begin(self, *, attach_opaque_data: bytes) -> TransactionOpaqueData: + with _Authority.lock: + if attach_opaque_data not in _Authority.attachments: + raise _error("unknown attachment") + token = TransactionOpaqueData(uuid.uuid4().bytes) + db = _Authority.connect() + db.execute("BEGIN") + _Authority.transactions[token] = _Transaction(attach_opaque_data, db) + _Authority.event("begin", attach=attach_opaque_data, token=token) + return token + + def _finish(self, operation: str, attach: bytes, token: bytes) -> None: + with _Authority.lock: + transaction = _Authority.transaction(attach, token) + _Authority.event("finish_attempt", attach=attach, token=token, phase=operation) + try: + transaction.connection.execute(operation.upper()) + except sqlite3.Error as exc: + raise _error(str(exc)) from exc + transaction.connection.close() + del _Authority.transactions[token] + for execution_id in [key for key, value in _Authority.executions.items() if value.token == token]: + del _Authority.executions[execution_id] + _Authority.event(operation, attach=attach, token=token) + + def catalog_transaction_commit(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes) -> None: + self._finish("commit", attach_opaque_data, transaction_opaque_data) + + def catalog_transaction_rollback(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes) -> None: + self._finish("rollback", attach_opaque_data, transaction_opaque_data) + + def schemas(self, *, attach_opaque_data: bytes, transaction_opaque_data: bytes | None) -> list[SchemaInfo]: + self.catalog_version(attach_opaque_data=attach_opaque_data, transaction_opaque_data=transaction_opaque_data) + return [ + SchemaInfo(attach_opaque_data=AttachOpaqueData(attach_opaque_data), path=["main"], comment=None, tags={}) + ] + + def schema_get(self, *, path: list[str], **kwargs: Any) -> SchemaInfo | None: + return self.schemas(**kwargs)[0] if path == ["main"] else None + + @staticmethod + def _info(name: str) -> TableInfo: + return TableInfo( + name=name, + schema_path=["main"], + columns=SerializedSchema(_ROWS.serialize().to_pybytes()), + constraints=[], + comment=None, + tags={}, + write_result_modes={"insert": "rows"}, + ) + + def table_get( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + schema_path: list[str], + name: str, + at_unit: str | None = None, + at_value: str | None = None, + ) -> TableInfo | None: + if at_unit is not None or at_value is not None: + raise _error("time travel is unsupported") + if schema_path != ["main"]: + return None + with _Authority.connection(attach_opaque_data, transaction_opaque_data) as db: + row = db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name=? COLLATE NOCASE", (name,) + ).fetchone() + return self._info(row[0]) if row else None + + def schema_contents( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + path: list[str], + type: SchemaObjectType, + ) -> Any: + if path != ["main"]: + return [] + if type == SchemaObjectType.TABLE: + with _Authority.connection(attach_opaque_data, transaction_opaque_data) as db: + return [ + self._info(row[0]) + for row in db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") + ] + return super().schema_contents( + attach_opaque_data=AttachOpaqueData(attach_opaque_data), + transaction_opaque_data=TransactionOpaqueData(transaction_opaque_data) if transaction_opaque_data else None, + path=path, + type=type, + ) + + def table_create( + self, + *, + attach_opaque_data: bytes, + transaction_opaque_data: bytes | None, + schema_path: list[str], + name: str, + columns: bytes, + on_conflict: OnConflict, + constraint_definitions: list[bytes], + **kwargs: Any, + ) -> None: + schema = pa.ipc.read_schema(pa.py_buffer(columns)) + if ( + schema_path != ["main"] + or on_conflict != OnConflict.ERROR + or constraint_definitions + or any(value is not None for value in kwargs.values()) + or not schema.equals(_ROWS, check_metadata=False) + ): + raise _error("CREATE requires unconstrained nullable (id BIGINT, label VARCHAR) without options") + with _Authority.lock: + _Authority.transaction(attach_opaque_data, transaction_opaque_data) + with _Authority.connection(attach_opaque_data, transaction_opaque_data) as db: + db.execute(f"CREATE TABLE {_identifier(name)} (id BIGINT, label VARCHAR)") + _Authority.event("table_create", attach=attach_opaque_data, token=transaction_opaque_data, table=name) + + def _function(self, kind: str, **kwargs: Any) -> ScanFunctionResult: + kwargs.pop("writable_branch_function_name", None) + info = self.table_get(**kwargs) + if info is None: + raise _error("unknown table") + return ScanFunctionResult( + function_name=f"transactional_write_{kind}", + schema_path=["main"], + positional_arguments=[pa.scalar(info.name)], + named_arguments={}, + ) + + def table_scan_function_get(self, **kwargs: Any) -> ScanFunctionResult: + return self._function("scan", **kwargs) + + def table_insert_function_get(self, **kwargs: Any) -> ScanFunctionResult: + return self._function("insert", **kwargs) + + +class TransactionalWriteWorker(Worker): + """Serve the test authority through the unchanged public protocol.""" + + catalog = CATALOG + catalog_interface = TransactionalWriteCatalog + + +def create_reference_app() -> Any: + """Build a signed WSGI application intended for exactly one worker process.""" + root = _root() + os.environ["VGI_WORKER_SHARED_STORAGE"] = "sqlite" + os.environ["VGI_WORKER_SQLITE_PATH"] = str(root / "framework.sqlite") + _Authority.initialize() + from vgi.serve import create_app + + return create_app(TransactionalWriteWorker, signing_key=bytes(range(32))) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--port", type=int, default=0) + args = parser.parse_args() + with make_server("127.0.0.1", args.port, create_reference_app()) as server: + print(f"PORT:{server.server_port}", flush=True) + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/tests/catalog/typing_helpers.py b/tests/catalog/typing_helpers.py new file mode 100644 index 00000000..e9e62bf2 --- /dev/null +++ b/tests/catalog/typing_helpers.py @@ -0,0 +1,53 @@ +# Copyright 2026 Query Farm LLC - https://query.farm + +"""Typed argument bundles shared by catalog protocol tests.""" + +from __future__ import annotations + +from typing import NotRequired, TypedDict + +from vgi.catalog.catalog_interface import AttachOpaqueData, TransactionOpaqueData + + +class CatalogPathArgs(TypedDict): + """Identify a catalog schema path.""" + + attach_opaque_data: AttachOpaqueData + schema_path: list[str] + + +class CatalogObjectArgs(CatalogPathArgs): + """Identify a catalog object.""" + + name: str + + +class CatalogObjectTransactionArgs(CatalogObjectArgs): + """Identify an object in an optional transaction.""" + + transaction_opaque_data: TransactionOpaqueData | None + + +class CatalogColumnTransactionArgs(CatalogObjectTransactionArgs): + """Optionally identify a nested column in a transaction.""" + + column_path: NotRequired[list[str]] + + +class CatalogColumnArgs(CatalogObjectArgs): + """Identify a nested column.""" + + column_path: list[str] + + +class CatalogSchemaArgs(TypedDict): + """Identify a schema for schema metadata calls.""" + + attach_opaque_data: AttachOpaqueData + path: list[str] + + +def present[T](value: T | None) -> T: + """Require an expected catalog result and narrow its optional type.""" + assert value is not None + return value diff --git a/tests/catalog/view_alias_native.py b/tests/catalog/view_alias_native.py new file mode 100644 index 00000000..840c61a7 --- /dev/null +++ b/tests/catalog/view_alias_native.py @@ -0,0 +1,146 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D106 +"""View metadata fixture for native alias validation; not a production catalog.""" + +from __future__ import annotations + +import os +from dataclasses import fields, replace +from typing import Any + +import pyarrow as pa + +from vgi.catalog import Catalog, ReadOnlyCatalogInterface, Schema, SchemaObjectType, View +from vgi.protocol import CatalogContentsResponse, ViewsResponse +from vgi.worker import Worker + +CATALOG = Catalog( + name="views", + default_schema="main", + schemas=[ + Schema( + path=["main"], views=[View(name="v", definition="SELECT 11 AS x, 22 AS y", aliases=["a.b", 'with"quote'])] + ) + ], +) + + +def corrupt_wire(raw: bytes | None) -> bytes | None: + """Change encoded item/collection streams after normal metadata validation.""" + mode = os.environ.get("VGI_VIEW_ALIAS_MODE") + security = os.environ.get("VGI_VIEW_SECURITY_MODE") + if raw is None or not (mode or security): + return raw + batches = [] + with pa.ipc.open_stream(raw) as reader: + for batch in reader: + column = "security" if security else "aliases" + index = batch.schema.get_field_index(column) + selected = security or mode + if selected == "missing": + batch = batch.remove_column(index) + elif security: + if security == "wrong_type": + field = pa.field("security", pa.int32()) + value: Any = 1 + else: + field = batch.schema.field(index) + value = None if security == "null" else security + batch = batch.set_column(index, field, pa.array([value] * batch.num_rows, type=field.type)) + else: + assert mode is not None + values: dict[str, Any] = { + "null": None, + "null_item": [None, "y"], + "empty": ["", "y"], + "nul": ["bad\0", "y"], + } + batch = batch.set_column( + index, + batch.schema.field(index), + pa.array([values[mode]] * batch.num_rows, type=pa.list_(pa.string())), + ) + batches.append(batch) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, batches[0].schema) as writer: + for batch in batches: + writer.write_batch(batch) + return sink.getvalue().to_pybytes() + + +class ViewsCatalog(ReadOnlyCatalogInterface): + """Expose the same view through get and listing.""" + + catalog = CATALOG + + def catalog_attach(self, **kwargs: Any) -> Any: + return replace( + super().catalog_attach(**kwargs), + supports_catalog_contents=os.environ.get("VGI_VIEW_METADATA_PATH") == "bulk", + ) + + _building_contents = False + + def catalog_contents(self, **kwargs: Any) -> Any: + self._building_contents = True + try: + result = super().catalog_contents(**kwargs) + finally: + self._building_contents = False + return result + + def view_get(self, **kwargs: Any) -> Any: + if os.environ.get("VGI_VIEW_METADATA_PATH") in ("listing", "bulk"): + raise AssertionError("unexpected single-view read in discovery test") + info = super().view_get(**kwargs) + return info + + def schema_contents(self, **kwargs: Any) -> Any: + path = os.environ.get("VGI_VIEW_METADATA_PATH") + if kwargs["type"] == SchemaObjectType.VIEW and ( + path == "get" or (path == "bulk" and not self._building_contents) + ): + raise AssertionError("unexpected schema-list read in view metadata test") + infos = super().schema_contents(**kwargs) + return infos + + +class ViewsWorker(Worker): + """Serve malformed metadata over the normal transport.""" + + catalog = CATALOG + catalog_interface = ViewsCatalog + + def catalog_view_get( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + transaction_opaque_data: bytes | None = None, + ) -> ViewsResponse: + result = super().catalog_view_get(attach_opaque_data, schema_path, name, transaction_opaque_data) + return replace(result, items=[corrupt_wire(item) for item in result.items]) + + def catalog_schema_contents_views( + self, + attach_opaque_data: bytes, + path: list[str], + transaction_opaque_data: bytes | None = None, + ) -> ViewsResponse: + result = super().catalog_schema_contents_views(attach_opaque_data, path, transaction_opaque_data) + return replace(result, items=[corrupt_wire(item) for item in result.items]) + + def catalog_contents( + self, + attach_opaque_data: bytes, + if_none_match: str | None = None, + ) -> CatalogContentsResponse: + result = super().catalog_contents(attach_opaque_data, if_none_match) + return CatalogContentsResponse( + **{field.name: getattr(result, field.name) for field in fields(CatalogContentsResponse)} + | {"views": corrupt_wire(result.views)} + ) + + +if __name__ == "__main__": + ViewsWorker.main() diff --git a/tests/catalog/view_security_reference.py b/tests/catalog/view_security_reference.py new file mode 100644 index 00000000..fddf1e0b --- /dev/null +++ b/tests/catalog/view_security_reference.py @@ -0,0 +1,160 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102 +"""Durable view metadata reference; this fixture does not execute view queries. + +The barrier read profile promises to preserve the flag and enforce it if a +consumer materializes the view. SQLite persistence alone does not qualify a +DuckDB 2 execution adapter. +""" + +from __future__ import annotations + +import base64 +import json +import sqlite3 +from contextlib import closing +from dataclasses import replace +from typing import Any, ClassVar + +from vgi.catalog import AttachOpaqueData, SchemaInfo, SchemaObjectType, TransactionOpaqueData, ViewInfo +from vgi.catalog.capabilities import VIEW_BARRIER_CREATE, VIEW_BARRIER_READ, CatalogCapabilities +from vgi.catalog.catalog_interface import CatalogContentsResult, OnConflict, SchemaContentsInfo +from vgi.worker import Worker + +from .schema_options_reference import PROFILES as SCHEMA_PROFILES +from .schema_options_reference import SchemaOptionsCatalog, _key + +PROFILES = replace( + SCHEMA_PROFILES, + read=sorted([*SCHEMA_PROFILES.read, VIEW_BARRIER_READ]), + ddl=sorted([*SCHEMA_PROFILES.ddl, VIEW_BARRIER_CREATE]), +) + + +def _view_key(path: list[str], name: str) -> str: + return "view:" + json.dumps([path, name]) + + +class ViewSecurityCatalog(SchemaOptionsCatalog): + """Store explicit security modes in the same atomic snapshots as schemas.""" + + profiles: ClassVar[CatalogCapabilities] = PROFILES + + def schemas(self, **kwargs: Any) -> list[SchemaInfo]: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + return [self._decode(value, kwargs["attach_opaque_data"]) for key, value in data.items() if key.startswith("[")] + + def _views(self, attach: AttachOpaqueData, tx: TransactionOpaqueData | None) -> list[ViewInfo]: + with closing(sqlite3.connect(self.database_path)) as db: + _, data = self._snapshot(db, attach, tx) + return [ + ViewInfo.deserialize_from_bytes(base64.b64decode(value)) + for key, value in data.items() + if key.startswith("view:") + ] + + def view_get(self, **kwargs: Any) -> ViewInfo | None: + return next( + ( + v + for v in self._views(kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + if v.schema_path == kwargs["schema_path"] and v.name == kwargs["name"] + ), + None, + ) + + def schema_contents(self, **kwargs: Any) -> Any: + if kwargs["type"] != SchemaObjectType.VIEW: + return [] + return [ + v + for v in self._views(kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"]) + if v.schema_path == kwargs["path"] + ] + + def catalog_contents(self, **kwargs: Any) -> CatalogContentsResult: + attach = kwargs["attach_opaque_data"] + views = self._views(attach, None) + return CatalogContentsResult( + schemas=[ + SchemaContentsInfo(schema=schema, views=[v for v in views if v.schema_path == schema.path]) + for schema in self.schemas(attach_opaque_data=attach, transaction_opaque_data=None) + ], + etag="deliberately-constant-provider-etag", + ) + + def _change_view(self, operation: str, **kwargs: Any) -> None: + attach, tx = kwargs["attach_opaque_data"], kwargs["transaction_opaque_data"] + path, name = kwargs["schema_path"], kwargs["name"] + key = _view_key(path, name) + with closing(sqlite3.connect(self.database_path)) as db, db: + db.execute("BEGIN IMMEDIATE") + version, data = self._snapshot(db, attach, tx) + if _key(path) not in data: + raise ValueError("schema does not exist") + if operation == "create": + info = ViewInfo( + name=name, + schema_path=path, + definition=kwargs["definition"], + aliases=list(kwargs.get("aliases") or []), + security=kwargs.get("security", "regular"), + comment=kwargs.get("comment"), + tags=dict(kwargs.get("tags") or {}), + column_comments=dict(kwargs.get("column_comments") or {}), + ) + if key in data: + if kwargs["on_conflict"] == OnConflict.IGNORE: + return + if kwargs["on_conflict"] != OnConflict.REPLACE: + raise ValueError("view already exists") + else: + if key not in data: + if kwargs["ignore_not_found"]: + return + raise ValueError("view does not exist") + info = ViewInfo.deserialize_from_bytes(base64.b64decode(data[key])) + if operation == "rename": + new_key = _view_key(path, kwargs["new_name"]) + if new_key in data: + raise ValueError("view already exists") + del data[key] + key, info = new_key, replace(info, name=kwargs["new_name"]) + elif operation == "column_comment": + path = kwargs["column_path"] + if len(path) != 1 or not path[0] or "\0" in path[0]: + raise ValueError("view column comments require one raw output identifier") + comments = dict(info.column_comments) + if kwargs["comment"] is None: + comments.pop(path[0], None) + else: + comments[path[0]] = kwargs["comment"] + info = replace(info, column_comments=comments) + elif operation == "comment": + info = replace(info, comment=kwargs["comment"]) + else: + raise AssertionError(operation) + data[key] = base64.b64encode(info.serialize_to_bytes()).decode() + if tx is None: + db.execute("UPDATE catalog SET version=?, data=? WHERE id=1", (version + 1, json.dumps(data))) + else: + db.execute("UPDATE transactions SET version=?, data=? WHERE id=?", (version + 1, json.dumps(data), tx)) + + def view_create(self, **kwargs: Any) -> None: + self._change_view("create", **kwargs) + + def view_rename(self, **kwargs: Any) -> None: + self._change_view("rename", **kwargs) + + def view_column_comment_set(self, **kwargs: Any) -> None: + self._change_view("column_comment", **kwargs) + + def view_comment_set(self, **kwargs: Any) -> None: + self._change_view("comment", **kwargs) + + +class ViewSecurityWorker(Worker): + """Expose the metadata reference through public RPC and sealed attachments.""" + + catalog_interface = ViewSecurityCatalog diff --git a/tests/catalog/write_finalize_probe.py b/tests/catalog/write_finalize_probe.py new file mode 100644 index 00000000..b91b2bc2 --- /dev/null +++ b/tests/catalog/write_finalize_probe.py @@ -0,0 +1,273 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D102, D106 +"""Opt-in native INSERT/finalize diagnostic; never a transactional provider. + +Run with ``VGI_WRITE_FINALIZE_PROBE_DIR=/absolute/isolated/directory`` and +``python -m tests.catalog.write_finalize_probe``. Attach catalog +``write_finalize_probe`` using that worker command. All five tables have +``id BIGINT, label VARCHAR`` and accept INSERT only: + +* ``input_result`` returns counts or RETURNING rows during INPUT. +* ``emit_then_raise`` buffers an INPUT response, then raises before returning. +* ``finalize_count`` returns zero during INPUT and the total count at FINALIZE. +* ``finalize_raise`` returns zero during INPUT, then raises at FINALIZE. +* ``deferred_returning`` returns no rows during INPUT and rows at FINALIZE; + native writes currently reject this final row schema as an invalid count. + +The first two functions have no finalize override. The other three advertise +one through the ordinary override detection. A zero-input INSERT should still +invoke an advertised finalize callback; ``finalize_count`` then returns zero. + +``events.jsonl`` records callback phases, function name, execution ID and PID. +Per-execution ``rows-*.jsonl`` files retain received input for final output. +The ``buffered_emit`` event means out.emit() returned, not that bytes reached +the client. These files are diagnostic observations, not transactional table +storage. SELECT, rollback, atomicity and recovery are deliberately unsupported. +The launcher also keeps framework SQLite storage in the configured directory. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + +import pyarrow as pa +from vgi_rpc.rpc import OutputCollector + +from vgi._test_fixtures.simple_writable import _parse_write_options +from vgi.catalog import Catalog, ReadOnlyCatalogInterface, Schema, Table +from vgi.invocation import BindResponse +from vgi.table_function import BindParams, ProcessParams, TableFunctionGenerator +from vgi.table_in_out_function import TableInOutGenerator +from vgi.worker import Worker +from vgi.write_results import write_result_schema + +_ROW_FIELDS: list[pa.Field[Any]] = [pa.field("id", pa.int64()), pa.field("label", pa.string())] +_ROWS = pa.schema(_ROW_FIELDS) +_COUNT = write_result_schema("count", _ROWS) + + +def _root() -> Path: + configured = os.environ.get("VGI_WRITE_FINALIZE_PROBE_DIR") + if not configured: + raise ValueError("VGI_WRITE_FINALIZE_PROBE_DIR must name an isolated absolute directory") + root = Path(configured) + if not root.is_absolute(): + raise ValueError("VGI_WRITE_FINALIZE_PROBE_DIR must be absolute") + root.mkdir(parents=True, exist_ok=True) + return root + + +def _append(path: Path, value: Any) -> None: + # Native writes serialize their batch callbacks. One append syscall also + # keeps diagnostics from distinct worker processes as separate JSON lines. + payload = (json.dumps(value, separators=(",", ":")) + "\n").encode() + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + try: + if os.write(descriptor, payload) != len(payload): + raise OSError("Incomplete diagnostic append") + finally: + os.close(descriptor) + + +def _execution(params: ProcessParams[None]) -> str: + if params.init_response is None: + raise ValueError("Diagnostic callback requires an execution ID") + return params.init_response.execution_id.hex() + + +def _rows_path(params: ProcessParams[None]) -> Path: + return _root() / f"rows-{_execution(params)}.jsonl" + + +def _received_rows(params: ProcessParams[None]) -> list[dict[str, Any]]: + path = _rows_path(params) + if not path.exists(): + return [] + with path.open(encoding="utf-8") as source: + return [row for line in source for row in json.loads(line)] + + +def _event(params: ProcessParams[None], phase: str, **details: Any) -> None: + assert params.init_call is not None + _append( + _root() / "events.jsonl", + { + "function": params.init_call.bind_call.function_name, + "execution_id": _execution(params), + "pid": os.getpid(), + "phase": phase, + **details, + }, + ) + + +def _count(value: int) -> pa.RecordBatch: + return pa.RecordBatch.from_pydict({"count": [value]}, schema=_COUNT) + + +class ProbeScan(TableFunctionGenerator[None, None]): + """Supply the schema needed to bind INSERT without promising readable rows.""" + + class Meta: + name = "probe_schema_only_scan" + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + del params + return BindResponse(output_schema=_ROWS) + + @classmethod + def process(cls, params: ProcessParams[None], state: None, out: OutputCollector) -> None: + del params, state, out + raise NotImplementedError("write_finalize_probe: schema-only scan; readable table storage is unsupported") + + +class _ProbeWrite(TableInOutGenerator[None, None]): + """One synchronous INPUT response, optionally followed by a callback error.""" + + case = "input_result" + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + mode = _parse_write_options(params)["result_mode"] + if mode not in {"count", "rows"}: + raise ValueError("Diagnostic write supports count and rows only") + if cls.case in {"finalize_count", "finalize_raise"} and mode != "count": + raise ValueError("This diagnostic table supports count only") + if cls.case == "deferred_returning" and mode != "rows": + raise ValueError("Use INSERT ... RETURNING * for deferred_returning") + return BindResponse(output_schema=write_result_schema(mode, _ROWS)) + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> None: + _event(params, "input_init") + + @classmethod + def process(cls, params: ProcessParams[None], state: None, batch: pa.RecordBatch, out: OutputCollector) -> None: + del state + mode = _parse_write_options(params)["result_mode"] + _event(params, "input", rows=batch.num_rows, result_mode=mode) + _append(_rows_path(params), batch.to_pylist()) + if cls.case == "deferred_returning": + response = pa.RecordBatch.from_pylist([], schema=_ROWS) + elif cls.case in {"finalize_count", "finalize_raise"}: + response = _count(0) + else: + response = batch if mode == "rows" else _count(batch.num_rows) + out.emit(response) + _event(params, "buffered_emit", rows=response.num_rows) + if cls.case == "emit_then_raise": + _event(params, "input_raise") + raise RuntimeError("write_finalize_probe: process raised after buffered emit") + _event(params, "input_return") + + +class InputResult(_ProbeWrite): + """Normal INPUT result, without a finalize callback.""" + + class Meta: + name = "probe_input_result" + + +class EmitThenRaise(_ProbeWrite): + """A buffered response must not escape a failing process callback.""" + + case = "emit_then_raise" + + class Meta: + name = "probe_emit_then_raise" + + +class _FinalizingWrite(_ProbeWrite): + """Use the existing optional FINALIZE callback, with no protocol additions.""" + + @classmethod + def finalize(cls, params: ProcessParams[None]) -> list[pa.RecordBatch]: + rows = _received_rows(params) + _event(params, "finalize", received_rows=len(rows)) + if cls.case == "finalize_raise": + _event(params, "finalize_raise") + raise RuntimeError("write_finalize_probe: finalize raised") + result = ( + pa.RecordBatch.from_pylist(rows, schema=_ROWS) if cls.case == "deferred_returning" else _count(len(rows)) + ) + _event(params, "finalize_return", rows=result.num_rows, columns=result.schema.names) + return [result] + + +class FinalizeCount(_FinalizingWrite): + """INPUT contributes zero; FINALIZE supplies the total received row count.""" + + case = "finalize_count" + + class Meta: + name = "probe_finalize_count" + + +class FinalizeRaise(_FinalizingWrite): + """INPUT completes normally, then the finalize callback fails.""" + + case = "finalize_raise" + + class Meta: + name = "probe_finalize_raise" + + +class DeferredReturning(_FinalizingWrite): + """Exercise the unsupported native writable final-row output path.""" + + case = "deferred_returning" + + class Meta: + name = "probe_deferred_returning" + + +_HANDLERS = [InputResult, EmitThenRaise, FinalizeCount, FinalizeRaise, DeferredReturning] +CATALOG = Catalog( + name="write_finalize_probe", + default_schema="main", + schemas=[ + Schema( + path=["main"], + functions=[ProbeScan, *_HANDLERS], + tables=[ + Table( + name=handler.case, + function=ProbeScan, + insert_function=handler, + write_result_modes={"insert": "count" if handler in (FinalizeCount, FinalizeRaise) else "rows"}, + ) + for handler in _HANDLERS + ], + ) + ], +) + + +class WriteFinalizeProbeCatalog(ReadOnlyCatalogInterface): + """Static diagnostic sinks; no transaction or readable-storage promises.""" + + catalog = CATALOG + supports_transactions = False + + +class WriteFinalizeProbeWorker(Worker): + """Serve the opt-in probe over the normal worker transports.""" + + catalog = CATALOG + catalog_interface = WriteFinalizeProbeCatalog + + +def main() -> None: + """Run with all diagnostic and framework state inside the requested directory.""" + root = _root() + os.environ["VGI_WORKER_SHARED_STORAGE"] = "sqlite" + os.environ["VGI_WORKER_SQLITE_PATH"] = str(root / "framework.sqlite") + WriteFinalizeProbeWorker.main() + + +if __name__ == "__main__": + main() diff --git a/tests/client/test_cli_catalog_functions.py b/tests/client/test_cli_catalog_functions.py index 4e5e7431..fc1955c5 100644 --- a/tests/client/test_cli_catalog_functions.py +++ b/tests/client/test_cli_catalog_functions.py @@ -103,7 +103,7 @@ def test_schema_contents_lists_table_functions(self, fixture_worker: str) -> Non assert contents_result.exit_code == 0 # Parse all output lines as JSON - lines = contents_result.output.strip().split("\n") + lines = contents_result.stdout.strip().split("\n") items = [json.loads(line) for line in lines if line.strip()] # Should have items @@ -164,7 +164,7 @@ def test_schema_contents_with_catalog_option(self, fixture_worker: str) -> None: assert contents_result.exit_code == 0 # Parse all output lines as JSON - lines = contents_result.output.strip().split("\n") + lines = contents_result.stdout.strip().split("\n") items = [json.loads(line) for line in lines if line.strip()] # Should have items @@ -287,7 +287,7 @@ def test_function_info_has_correct_structure(self, fixture_worker: str) -> None: assert contents_result.exit_code == 0 # Parse first function - lines = contents_result.output.strip().split("\n") + lines = contents_result.stdout.strip().split("\n") first_func = json.loads(lines[0]) # Check expected fields @@ -327,7 +327,7 @@ def test_scalar_functions_have_correct_type(self, fixture_worker: str) -> None: assert contents_result.exit_code == 0 # Parse scalar functions - lines = contents_result.output.strip().split("\n") + lines = contents_result.stdout.strip().split("\n") items = [json.loads(line) for line in lines if line.strip()] by_name = {item["name"]: item for item in items} @@ -365,7 +365,7 @@ def test_table_functions_have_correct_type(self, fixture_worker: str) -> None: assert contents_result.exit_code == 0 # Parse table functions - lines = contents_result.output.strip().split("\n") + lines = contents_result.stdout.strip().split("\n") items = [json.loads(line) for line in lines if line.strip()] by_name = {item["name"]: item for item in items} @@ -405,7 +405,7 @@ def test_varargs_function_shows_varargs_in_arguments(self, fixture_worker: str) assert contents_result.exit_code == 0 # Parse and find sum_values function - lines = contents_result.output.strip().split("\n") + lines = contents_result.stdout.strip().split("\n") items = [json.loads(line) for line in lines if line.strip()] by_name = {item["name"]: item for item in items} @@ -455,7 +455,7 @@ def test_schema_list_shows_main(self, fixture_worker: str) -> None: # Parse schema info (NDJSON - one JSON object per line) # Get the first schema which should be "main" - lines = list_result.output.strip().split("\n") + lines = list_result.stdout.strip().split("\n") schema_info = json.loads(lines[0]) assert schema_info["path"] == ["main"] @@ -480,7 +480,7 @@ def test_schema_list_with_catalog_option(self, fixture_worker: str) -> None: # Parse schema info (NDJSON - one JSON object per line) # Get the first schema which should be "main" - lines = list_result.output.strip().split("\n") + lines = list_result.stdout.strip().split("\n") schema_info = json.loads(lines[0]) assert schema_info["path"] == ["main"] diff --git a/tests/conformance/test_directory_parity.py b/tests/conformance/test_directory_parity.py index ad7e363f..ed123bad 100644 --- a/tests/conformance/test_directory_parity.py +++ b/tests/conformance/test_directory_parity.py @@ -25,6 +25,11 @@ # Areas the Python conformance suite intentionally does not mirror. Each entry # must cite a reason — leaving an empty reason defeats the purpose of the test. _EXEMPTIONS: dict[str, str] = { + "attach_ticket": ( + "Authenticated ticket minting/redemption, identity and expiry are exercised by " + "tests/test_attach_ticket.py and tests/catalog/test_client_capabilities.py; " + "these require explicit keys/identities rather than the generic fixture client." + ), "catalog": ( "Multi-branch scan planning (multi_branch_*) is DuckDB-only — the C++ " "extension drives catalog_table_scan_branches_get to route scans across " diff --git a/tests/conformance/test_protocol_inventory.py b/tests/conformance/test_protocol_inventory.py index 2893c69d..a0ef0c99 100644 --- a/tests/conformance/test_protocol_inventory.py +++ b/tests/conformance/test_protocol_inventory.py @@ -70,6 +70,14 @@ class NotExposed: "Covered by C++ integration/table_function paths." ) ), + "table_function_projection": NotExposed( + reason=( + "Planner-side scalar expression pushdown. The Python Client executes " + "table functions directly without rewriting a logical projection. " + "Covered by tests/test_projection_expressions.py and native " + "test/run_projection_pushdown.py." + ) + ), "table_function_plan": NotExposed( reason=( "Engine-side scan planning: splits the scan into named, independently " @@ -117,6 +125,9 @@ class NotExposed: "catalog_create": ("catalog_create",), "catalog_drop": ("catalog_drop",), "catalog_version": ("catalog_version",), + "catalog_dependencies": ("catalog_dependencies",), + "catalog_query_prepare": ("catalog_query_prepare",), + "catalog_query_release": ("catalog_query_release",), "catalog_transaction_begin": ("catalog_transaction_begin",), "catalog_transaction_commit": ("catalog_transaction_commit",), "catalog_transaction_rollback": ("catalog_transaction_rollback",), @@ -125,22 +136,23 @@ class NotExposed: "catalog_contents": ("contents",), "catalog_schema_get": ("schema_get",), "catalog_schema_create": ("schema_create",), + "catalog_schema_option_definitions": ("schema_option_definitions",), + "catalog_schema_alter_options": ("schema_alter_options",), + "catalog_schema_alter_metadata": ("schema_alter_metadata",), "catalog_schema_drop": ("schema_drop",), "catalog_schema_contents_tables": ("schema_contents",), "catalog_schema_contents_views": ("schema_contents",), "catalog_schema_contents_functions": ("schema_contents",), "catalog_schema_contents_macros": ("schema_contents",), "catalog_copy_from_formats": ("copy_formats",), - "catalog_schema_contents_indexes": NotExposed( - reason=( - "DuckDB-only metadata path. Indexes are catalog-planner territory; " - "non-DuckDB clients don't surface index listings. Covered by C++ " - "integration tests." - ) - ), + "catalog_schema_contents_indexes": ("schema_contents",), # ---------- Catalog tables ---------- + "catalog_table_option_definitions": ("table_option_definitions",), + "catalog_table_alter_options": ("table_alter_options",), + "catalog_table_alter_layout": ("table_alter_layout",), "catalog_table_get": ("table_get",), "catalog_table_create": ("table_create",), + "catalog_table_constraints_apply": ("table_constraints_apply",), "catalog_table_drop": ("table_drop",), "catalog_table_scan_function_get": ("table_scan_function_get",), "catalog_table_scan_branches_get": NotExposed( @@ -179,12 +191,7 @@ class NotExposed: ), ), "catalog_table_comment_set": ("table_comment_set",), - "catalog_table_column_comment_set": NotExposed( - reason=( - "DuckDB-only metadata path. Column comments travel with DDL; not a " - "non-DuckDB-client concern. Add a wrapper here if a use case arises." - ) - ), + "catalog_table_column_comment_set": ("table_column_comment_set",), "catalog_table_rename": ("table_rename",), "catalog_table_column_add": ("table_column_add",), "catalog_table_column_drop": ("table_column_drop",), @@ -200,20 +207,16 @@ class NotExposed: "catalog_view_drop": ("view_drop",), "catalog_view_rename": ("view_rename",), "catalog_view_comment_set": ("view_comment_set",), + "catalog_view_column_comment_set": ("view_column_comment_set",), # ---------- Catalog macros ---------- "catalog_macro_get": ("macro_get",), "catalog_macro_create": ("macro_create",), "catalog_macro_drop": ("macro_drop",), # ---------- Catalog indexes ---------- - "catalog_index_get": NotExposed( - reason=("DuckDB-only metadata path. Indexes are catalog-planner territory. Covered by C++ integration tests.") - ), - "catalog_index_create": NotExposed( - reason=("DuckDB-only metadata path. Indexes are catalog-planner territory. Covered by C++ integration tests.") - ), - "catalog_index_drop": NotExposed( - reason=("DuckDB-only metadata path. Indexes are catalog-planner territory. Covered by C++ integration tests.") - ), + "catalog_index_get": ("index_get",), + "catalog_index_option_definitions": ("index_option_definitions",), + "catalog_index_create": ("index_create",), + "catalog_index_drop": ("index_drop",), } diff --git a/tests/conformance/test_protocol_version.py b/tests/conformance/test_protocol_version.py index 4a447a9c..508c7e76 100644 --- a/tests/conformance/test_protocol_version.py +++ b/tests/conformance/test_protocol_version.py @@ -32,14 +32,15 @@ def test_client_protocol_version_is_canonical() -> None: """Sanity-check the constants this test reasons about haven't drifted apart.""" - assert CLIENT_PROTOCOL_VERSION == "2.2.0" + assert CLIENT_PROTOCOL_VERSION == "3.0.0" + assert VgiProtocol.protocol_name == "vgi.v3" # The fixture must be a genuine mismatch in the major/minor that the # framework compares; a patch-only difference would (correctly) pass. assert BAD_PROTOCOL_VERSION.split(".")[:2] != CLIENT_PROTOCOL_VERSION.split(".")[:2] def test_mismatched_worker_protocol_version_raises() -> None: - """A worker enforcing 99.0.0 must reject the 2.2.0 client at dispatch.""" + """A worker enforcing 99.0.0 must reject the 3.0.0 client at dispatch.""" with Client("vgi-fixture-bad-protocol-worker") as client, pytest.raises(ClientError) as exc_info: list( client.table_function( @@ -54,5 +55,5 @@ def test_mismatched_worker_protocol_version_raises() -> None: assert "protocol_version mismatch" in message assert CLIENT_PROTOCOL_VERSION in message assert BAD_PROTOCOL_VERSION in message - # Directional hint: the client (2.2.0) is older than the worker (99.0.0). + # Directional hint: the client (3.0.0) is older than the worker (99.0.0). assert "upgrade the VGI extension/client" in message diff --git a/tests/projection_reference.py b/tests/projection_reference.py new file mode 100644 index 00000000..7bb231f9 --- /dev/null +++ b/tests/projection_reference.py @@ -0,0 +1,280 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D101, D102, D103, D106, D107 + +"""Finite pure projection proposals with normal bind/init/HTTP stream lifetime.""" + +from __future__ import annotations + +import json +import os +import re +from dataclasses import dataclass, field +from typing import Any, ClassVar + +import pyarrow as pa +import pyarrow.compute as pc +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import OutputCollector + +from vgi import Worker +from vgi.catalog import Catalog, Schema, Table +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.invocation import BindRepeatability, BindResponse, GlobalInitResponse +from vgi.protocol import TableFunctionProjectionRequest, TableFunctionProjectionResponse +from vgi.table_function import BindParams, InitParams, ProcessParams, TableFunctionGenerator + +SOURCE_FIELDS: list[pa.Field[Any]] = [ + pa.field("n", pa.int32(), metadata={b"source": b"physical"}), + pa.field("other", pa.int32()), + pa.field("text", pa.string()), +] +SOURCE = pa.schema(SOURCE_FIELDS, metadata={b"provider": b"projection-reference"}) +ROWS: list[dict[str, int | str | None]] = [ + {"n": -2147483648, "other": 2, "text": "min"}, + {"n": 0, "other": 3, "text": "zero"}, + {"n": None, "other": None, "text": None}, + {"n": 2147483647, "other": -1, "text": "max"}, +] +BASE = b"projection-reference-source-v1" + + +def event(kind: str, function: str, **details: object) -> None: + filename = os.environ.get("VGI_PROJECTION_TRACE") + if filename: + with open(filename, "a") as output: + output.write(json.dumps({"event": kind, "function": function, **details}) + "\n") + + +@dataclass +class ProjectionPlan(ArrowSerializableDataclass): + """Complete portable replacement state; no prepared resource registry.""" + + request: bytes + accepted_indices: list[int] + + +@dataclass +class ProjectionState(ArrowSerializableDataclass): + """Continue one fixed source through ordinary HTTP stream serialization.""" + + offset: int = 0 + request: bytes | None = None + accepted_indices: list[int] = field(default_factory=list) + + +class ProjectionSource(TableFunctionGenerator[None, ProjectionState]): + """Qualify only total INTEGER-to-BIGINT widening casts against this source.""" + + mode: ClassVar[str] = "accepted" + repeatability: ClassVar[BindRepeatability] = BindRepeatability.UNKNOWN + source: ClassVar[pa.Schema] = SOURCE + rows: ClassVar[list[dict[str, int | str | None]]] = ROWS + + class Meta: + name = "projection_source" + projection_pushdown = False + filter_pushdown = False + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + event("bind", cls.Meta.name) + return BindResponse( + output_schema=cls.source, + opaque_data=BASE, + projection_expression_profiles=[] if cls.mode == "unoffered" else [ROW_SCALAR_EXPRESSION], + repeatability=cls.repeatability, + ) + + @classmethod + def candidate(cls, request: TableFunctionProjectionRequest, index: int) -> str | None: + expression = request.expression_records()[index] + if expression.evaluation_context != QueryEvaluationContext(): + return None + match = re.fullmatch(r'CAST\s*\(\s*(?:"(n|other)"|(n|other))\s+AS\s+BIGINT\s*\)', expression.sql.strip(), re.I) + if match is None: + return None + name = (match.group(1) or match.group(2)).lower() + if name not in cls.source.names or cls.source.field(name).type != pa.int32(): + return None + requested = request.result_schema.field(index) + if requested.type != pa.int64() or not requested.nullable: + return None + return name + + @classmethod + def on_projection( + cls, params: BindParams[None], request: TableFunctionProjectionRequest + ) -> TableFunctionProjectionResponse: + event("proposal", cls.Meta.name) + if ( + not request.bind_result.output_schema.equals(cls.source, check_metadata=True) + or request.bind_result.opaque_data != BASE + ): + raise ValueError("proposal changed the actual source bind contract") + if cls.mode in {"declined", "unoffered"}: + return TableFunctionProjectionResponse() + accepted = [i for i in range(len(request.expressions)) if cls.candidate(request, i) is not None] + if cls.mode == "partial": + accepted = accepted[:1] + if not accepted: + return TableFunctionProjectionResponse() + schema = pa.schema( + [*cls.source, *(request.result_schema.field(i) for i in accepted)], + metadata={k: v for k, v in (cls.source.metadata or {}).items()}, + ) + if cls.mode == "malformed": + schema = schema.set(len(cls.source), schema.field(len(cls.source)).with_type(pa.string())) + state = ProjectionPlan(request=request.serialize_to_bytes(), accepted_indices=accepted) + event("accepted", cls.Meta.name, accepted_indices=accepted, output_fields=schema.names) + return TableFunctionProjectionResponse( + accepted_indices=accepted, + bind_result=BindResponse( + output_schema=schema, + opaque_data=state.serialize_to_bytes(), + projection_expression_profiles=request.bind_result.projection_expression_profiles, + repeatability=request.bind_result.repeatability, + ), + ) + + @classmethod + def plan(cls, params: InitParams[None] | ProcessParams[None]) -> ProjectionPlan | None: + assert params.init_call is not None + payload = params.init_call.bind_opaque_data + if payload == BASE: + if not params.init_call.output_schema.equals(cls.source, check_metadata=True): + raise ValueError("ordinary source init changed its bind schema") + return None + assert payload is not None + plan = ProjectionPlan.deserialize_from_bytes(payload) + request = TableFunctionProjectionRequest.deserialize_from_bytes(plan.request) + result = TableFunctionProjectionResponse( + accepted_indices=plan.accepted_indices, + bind_result=BindResponse( + output_schema=params.init_call.output_schema, + opaque_data=payload, + projection_expression_profiles=request.bind_result.projection_expression_profiles, + repeatability=request.bind_result.repeatability, + ), + ) + result.validate_request(request) + if request.bind_call.serialize_to_bytes() != params.init_call.bind_call.serialize_to_bytes(): + raise ValueError("projection plan belongs to another source bind") + if any(cls.candidate(request, i) is None for i in plan.accepted_indices): + raise ValueError("projection plan contains an unqualified expression") + return plan + + @classmethod + def on_init(cls, params: InitParams[None]) -> GlobalInitResponse: + event("init", cls.Meta.name, output_fields=params.init_call.output_schema.names) + cls.plan(params) + if cls.mode == "execution_error": + raise RuntimeError("projection reference execution failed after acceptance") + return GlobalInitResponse(max_workers=1) + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> ProjectionState: + plan = cls.plan(params) + return ProjectionState( + request=None if plan is None else plan.request, + accepted_indices=[] if plan is None else plan.accepted_indices, + ) + + @classmethod + def process(cls, params: ProcessParams[None], state: ProjectionState, out: OutputCollector) -> None: + if state.offset >= len(cls.rows): + out.finish() + return + event("rows", cls.Meta.name) + source = pa.RecordBatch.from_pylist(cls.rows[state.offset : state.offset + 2], schema=cls.source) + values = list(source.columns) + if state.request is not None: + request = TableFunctionProjectionRequest.deserialize_from_bytes(state.request) + for index in state.accepted_indices: + name = cls.candidate(request, index) + assert name is not None + values.append(pc.cast(source.column(name), pa.int64(), safe=True)) + out.emit(pa.RecordBatch.from_arrays(values, schema=params.output_schema)) + state.offset += source.num_rows + + +class Accepted(ProjectionSource): + class Meta(ProjectionSource.Meta): + name = "projection_accepted" + + +class Declined(ProjectionSource): + mode = "declined" + + class Meta(ProjectionSource.Meta): + name = "projection_declined" + + +class Partial(ProjectionSource): + mode = "partial" + + class Meta(ProjectionSource.Meta): + name = "projection_partial" + + +class Unoffered(ProjectionSource): + mode = "unoffered" + + class Meta(ProjectionSource.Meta): + name = "projection_unoffered" + + +class Malformed(ProjectionSource): + mode = "malformed" + + class Meta(ProjectionSource.Meta): + name = "projection_malformed" + + +class ExecutionError(ProjectionSource): + mode = "execution_error" + + class Meta(ProjectionSource.Meta): + name = "projection_execution_error" + + +class ProjectionSequence(ProjectionSource): + repeatability = BindRepeatability.STABLE_WITHIN_QUERY + source = pa.schema([pa.field("n", pa.int32())]) + rows = [{"n": n} for n in range(10)] + + class Meta(ProjectionSource.Meta): + name = "projection_sequence" + + +CLASSES = [Accepted, Declined, Partial, Unoffered, Malformed, ExecutionError] +CATALOG = Catalog( + name="projection_reference", + schemas=[Schema(path=["main"], functions=CLASSES, tables=[Table(name=cls.mode, function=cls) for cls in CLASSES])], +) +COMMON = Catalog( + name="example", + schemas=[ + Schema( + path=["main"], + functions=[ProjectionSequence], + tables=[Table(name="projection_rows", function=ProjectionSequence)], + ) + ], +) + + +class ProjectionReferenceWorker(Worker): + """Exercise actual function-backed catalog scans without generated columns.""" + + catalog = CATALOG + + +class CommonProjectionWorker(Worker): + """Share the same small public-RPC source across SDK conformance tests.""" + + catalog = COMMON + + +if __name__ == "__main__": + (CommonProjectionWorker if os.environ.get("VGI_PROJECTION_COMMON") == "1" else ProjectionReferenceWorker).main() diff --git a/tests/repeatability_reference.py b/tests/repeatability_reference.py new file mode 100644 index 00000000..afa51f18 --- /dev/null +++ b/tests/repeatability_reference.py @@ -0,0 +1,299 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D101, D102, D103, D106, D107 + +"""Finite SQLite snapshot proof using only ordinary bind and stream state. + +VGI_REPEATABILITY_DB names an isolated absolute SQLite path. The native harness +may use seed_database/replace_rows from a separate process to commit mutations. +No resources are retained after bind; stable state contains its complete rows. +""" + +from __future__ import annotations + +import json +import os +import sqlite3 +from collections.abc import Sequence +from contextlib import closing +from dataclasses import dataclass, field, replace +from pathlib import Path +from typing import Any, ClassVar + +import pyarrow as pa +import pyarrow.compute as pc +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc import OutputCollector + +from vgi import Worker +from vgi.catalog import Catalog, Schema, Table, TableInfo +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.invocation import BindRepeatability, BindResponse, GlobalInitResponse +from vgi.protocol import TableFunctionProjectionRequest, TableFunctionProjectionResponse +from vgi.table_function import BindParams, InitParams, ProcessParams, TableFunctionGenerator + +SOURCE_FIELDS: list[pa.Field[Any]] = [ + pa.field("n", pa.int32()), + pa.field("label", pa.string()), + pa.field("generation", pa.int64()), +] +SOURCE = pa.schema(SOURCE_FIELDS) +BASELINE: list[tuple[int | None, str]] = [(1, "one"), (None, "null"), (3, "three"), (4, "four")] + + +def database_path() -> Path: + path = Path(os.environ["VGI_REPEATABILITY_DB"]) + if not path.is_absolute(): + raise ValueError("VGI_REPEATABILITY_DB must be absolute") + return path + + +def seed_database(path: Path, rows: Sequence[tuple[int | None, str]] = BASELINE, *, generation: int = 1) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with closing(sqlite3.connect(path)) as db, db: + db.execute("CREATE TABLE IF NOT EXISTS version (generation INTEGER NOT NULL)") + db.execute("CREATE TABLE IF NOT EXISTS items (position INTEGER PRIMARY KEY, n INTEGER, label TEXT)") + db.execute("DELETE FROM version") + db.execute("INSERT INTO version VALUES (?)", [generation]) + db.execute("DELETE FROM items") + db.executemany("INSERT INTO items VALUES (?, ?, ?)", [(i, n, label) for i, (n, label) in enumerate(rows)]) + + +def replace_rows(path: Path, rows: Sequence[tuple[int | None, str]]) -> int: + with closing(sqlite3.connect(path)) as db, db: + db.execute("BEGIN IMMEDIATE") + generation = int(db.execute("SELECT generation FROM version").fetchone()[0]) + 1 + db.execute("UPDATE version SET generation = ?", [generation]) + db.execute("DELETE FROM items") + db.executemany("INSERT INTO items VALUES (?, ?, ?)", [(i, n, label) for i, (n, label) in enumerate(rows)]) + return generation + + +def event(kind: str, **details: object) -> None: + if filename := os.environ.get("VGI_REPEATABILITY_TRACE"): + with open(filename, "a") as output: + output.write(json.dumps({"event": kind, **details}) + "\n") + + +@dataclass +class Snapshot(ArrowSerializableDataclass): + generation: int + rows: bytes + + def batch(self) -> pa.RecordBatch: + with pa.ipc.open_stream(self.rows) as reader: + batches = list(reader) + if len(batches) != 1 or not batches[0].schema.equals(SOURCE, check_metadata=True): + raise ValueError("invalid repeatability snapshot") + return batches[0] + + +def read_snapshot(path: Path) -> Snapshot: + with closing(sqlite3.connect(path)) as db, db: + db.execute("BEGIN") + generation = db.execute("SELECT generation FROM version").fetchone()[0] + rows = db.execute("SELECT n, label FROM items ORDER BY position").fetchall() + return snapshot_from_rows(rows, generation) + + +def snapshot_from_rows(rows: Sequence[tuple[int | None, str]], generation: int) -> Snapshot: + batch = pa.RecordBatch.from_pylist( + [{"n": n, "label": label, "generation": generation} for n, label in rows], schema=SOURCE + ) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, SOURCE) as writer: + writer.write_batch(batch) + return Snapshot(generation=generation, rows=sink.getvalue().to_pybytes()) + + +@dataclass +class BoundState(ArrowSerializableDataclass): + snapshot: bytes | None = None + proposal: bytes | None = None + accepted_indices: list[int] = field(default_factory=list) + + +@dataclass +class StreamState(ArrowSerializableDataclass): + snapshot: bytes + accepted_indices: list[int] + offset: int = 0 + + +class RepeatabilitySource(TableFunctionGenerator[None, StreamState]): + repeatability: ClassVar[BindRepeatability] = BindRepeatability.UNKNOWN + projection_profiles: ClassVar[list[str]] = [ROW_SCALAR_EXPRESSION] + + class Meta: + name = "repeatability_source" + projection_pushdown = False + filter_pushdown = False + + @classmethod + def on_bind(cls, params: BindParams[None]) -> BindResponse: + snapshot = ( + read_snapshot(database_path()) if cls.repeatability == BindRepeatability.STABLE_WITHIN_QUERY else None + ) + event("bind", function=cls.Meta.name, generation=None if snapshot is None else snapshot.generation) + return BindResponse( + output_schema=SOURCE, + opaque_data=BoundState( + snapshot=None if snapshot is None else snapshot.serialize_to_bytes() + ).serialize_to_bytes(), + projection_expression_profiles=list(cls.projection_profiles), + repeatability=cls.repeatability, + ) + + @classmethod + def on_projection( + cls, params: BindParams[None], request: TableFunctionProjectionRequest + ) -> TableFunctionProjectionResponse: + if not request.bind_result.output_schema.equals(SOURCE, check_metadata=True): + raise ValueError("repeatability proposal changed source schema") + if request.bind_result.repeatability != cls.repeatability: + raise ValueError("repeatability proposal changed source promise") + assert request.bind_result.opaque_data is not None + original = BoundState.deserialize_from_bytes(request.bind_result.opaque_data) + if original.proposal is not None: + raise ValueError("repeatability reference negotiates once") + indices = [ + i + for i, expression in enumerate(request.expression_records()) + if expression.sql == 'CAST("n" AS BIGINT)' + and expression.evaluation_context == QueryEvaluationContext() + and request.result_schema.field(i).type == pa.int64() + and request.result_schema.field(i).nullable + ] + event("proposal", function=cls.Meta.name, accepted_indices=indices) + if not indices: + return TableFunctionProjectionResponse() + state = BoundState(snapshot=original.snapshot, proposal=request.serialize_to_bytes(), accepted_indices=indices) + return TableFunctionProjectionResponse( + accepted_indices=indices, + bind_result=BindResponse( + output_schema=pa.schema([*SOURCE, *(request.result_schema.field(i) for i in indices)]), + opaque_data=state.serialize_to_bytes(), + projection_expression_profiles=request.bind_result.projection_expression_profiles, + repeatability=request.bind_result.repeatability, + ), + ) + + @classmethod + def bound_state(cls, params: InitParams[None] | ProcessParams[None]) -> BoundState: + assert params.init_call is not None + assert params.init_call.bind_opaque_data is not None + state = BoundState.deserialize_from_bytes(params.init_call.bind_opaque_data) + if (state.snapshot is not None) != (cls.repeatability == BindRepeatability.STABLE_WITHIN_QUERY): + raise ValueError("repeatability reference requires its complete snapshot state") + if state.proposal is None: + if not params.init_call.output_schema.equals(SOURCE, check_metadata=True) or state.accepted_indices: + raise ValueError("repeatability source init changed its bind schema") + else: + proposal = TableFunctionProjectionRequest.deserialize_from_bytes(state.proposal) + if proposal.bind_call.serialize_to_bytes() != params.init_call.bind_call.serialize_to_bytes(): + raise ValueError("repeatability projection belongs to another bind") + TableFunctionProjectionResponse( + accepted_indices=state.accepted_indices, + bind_result=BindResponse( + output_schema=params.init_call.output_schema, + opaque_data=params.init_call.bind_opaque_data, + projection_expression_profiles=proposal.bind_result.projection_expression_profiles, + repeatability=cls.repeatability, + ), + ).validate_request(proposal) + return state + + @classmethod + def on_init(cls, params: InitParams[None]) -> GlobalInitResponse: + cls.bound_state(params) + return GlobalInitResponse(max_workers=1) + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> StreamState: + bound = cls.bound_state(params) + snapshot = ( + Snapshot.deserialize_from_bytes(bound.snapshot) + if bound.snapshot is not None + else read_snapshot(database_path()) + ) + snapshot.batch() + event("init", function=cls.Meta.name, generation=snapshot.generation) + return StreamState(snapshot=snapshot.serialize_to_bytes(), accepted_indices=bound.accepted_indices) + + @classmethod + def process(cls, params: ProcessParams[None], state: StreamState, out: OutputCollector) -> None: + snapshot = Snapshot.deserialize_from_bytes(state.snapshot) + batch = snapshot.batch() + if state.offset >= batch.num_rows: + out.finish() + return + batch = batch.slice(state.offset, 2) + columns = list(batch.columns) + for _ in state.accepted_indices: + columns.append(pc.cast(batch.column("n"), pa.int64(), safe=True)) + out.emit(pa.RecordBatch.from_arrays(columns, schema=params.output_schema)) + state.offset += batch.num_rows + event("rows", function=cls.Meta.name, generation=snapshot.generation, rows=batch.num_rows) + + +class Stable(RepeatabilitySource): + repeatability = BindRepeatability.STABLE_WITHIN_QUERY + + class Meta(RepeatabilitySource.Meta): + name = "repeatability_stable" + + +class StableUnoffered(Stable): + projection_profiles = [] + + class Meta(Stable.Meta): + name = "repeatability_stable_unoffered" + + +class Unknown(RepeatabilitySource): + class Meta(RepeatabilitySource.Meta): + name = "repeatability_unknown" + + +class SnapshotTable(Table): + """Declare the fixed source schema without taking a discovery-time snapshot.""" + + def _get_resolved_columns(self) -> pa.Schema: + return SOURCE + + +class StaleInlineTable(SnapshotTable): + """Publish an obvious stale snapshot to qualify native fresh-bind bypass.""" + + def to_table_info(self, schema_path: list[str]) -> TableInfo: + stale = snapshot_from_rows([(-99, "stale-inline")], 0) + bound = BindResponse( + output_schema=SOURCE, + opaque_data=BoundState(snapshot=stale.serialize_to_bytes()).serialize_to_bytes(), + repeatability=BindRepeatability.STABLE_WITHIN_QUERY, + ) + return replace(super().to_table_info(schema_path), bind_result=bound.serialize_to_bytes()) + + +class RepeatabilityWorker(Worker): + catalog = Catalog( + name="repeatability_reference", + schemas=[ + Schema( + path=["main"], + functions=[Stable, StableUnoffered, Unknown], + tables=[ + SnapshotTable(name="stable", function=Stable), + SnapshotTable(name="unknown", function=Unknown), + SnapshotTable(name="stable_unoffered", function=StableUnoffered), + StaleInlineTable(name="stable_inline", function=StableUnoffered), + ], + ) + ], + ) + + +if __name__ == "__main__": + if not database_path().exists(): + seed_database(database_path()) + RepeatabilityWorker.main() diff --git a/tests/scan_truncation_probe.py b/tests/scan_truncation_probe.py new file mode 100644 index 00000000..463f0df1 --- /dev/null +++ b/tests/scan_truncation_probe.py @@ -0,0 +1,56 @@ +# Copyright 2026 Query Farm LLC - https://query.farm + +"""Private native scan probe: terminate a real worker before producer EOS. + +The first generator tick goes through the ordinary filtering, collection and +Arrow writer. The next tick exits the process without closing that writer, so +the client receives a physical transport EOF after a valid empty or nonempty +batch. This must fail, never return a successful partial query result. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any +from unittest.mock import patch + +from vgi._test_fixtures.table.sequence import NestedSequenceFunction +from vgi._test_fixtures.table.splits import SplitPaginatedFunction +from vgi._test_fixtures.worker import main as fixture_main + +_nested_process = NestedSequenceFunction.process +_split_process = SplitPaginatedFunction.process + + +def _process(cls: type[Any], params: Any, state: Any, out: Any) -> None: + del cls + if os.environ.get("VGI_SCAN_TRUNCATION_MODE") == "drop" and state.current_index >= 100: + Path(os.environ["VGI_SCAN_TRUNCATION_EVENT"]).write_text( + f"pid={os.getpid()} next_index={state.current_index}\n", encoding="utf-8" + ) + os._exit(75) + _nested_process(params, state, out) + + +def _process_split(cls: type[Any], params: Any, state: Any, out: Any) -> None: + del cls + if os.environ.get("VGI_SCAN_TRUNCATION_MODE") == "drop_split" and state.cur >= state.hi[state.idx]: + Path(os.environ["VGI_SCAN_TRUNCATION_EVENT"]).write_text( + f"pid={os.getpid()} split_end={state.cur}\n", encoding="utf-8" + ) + os._exit(75) + _split_process(params, state, out) + + +def main() -> None: + """Apply failure injection only inside this separately launched test worker.""" + with ( + patch.object(NestedSequenceFunction, "process", classmethod(_process)), + patch.object(SplitPaginatedFunction, "process", classmethod(_process_split)), + ): + fixture_main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_argument_metadata.py b/tests/test_argument_metadata.py new file mode 100644 index 00000000..5cbc4cae --- /dev/null +++ b/tests/test_argument_metadata.py @@ -0,0 +1,156 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Arguments retain Arrow metadata through binding, init, and HTTP transport.""" + +from __future__ import annotations + +from dataclasses import replace + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient +from vgi_rpc.rpc import CallContext + +from vgi.arguments import Arguments +from vgi.client import Client +from vgi.invocation import BindResponse, FunctionType +from vgi.protocol import BindRequest, InitRequest +from vgi.serve import create_app +from vgi.typed_values import TypedValueBatch +from vgi.worker import Worker + + +def _arguments() -> pa.RecordBatch: + schema = pa.schema( + [ + pa.field("named_a.b", pa.int32(), metadata={b"logical": b"provider.optional"}), + pa.field( + "positional_0", + pa.struct([pa.field("child", pa.string(), metadata={b"nested": b"preserved"})]), + nullable=False, + metadata={b"logical": b"provider.record"}, + ), + ], + metadata={b"context": b"preserved"}, + ) + return pa.RecordBatch.from_pylist([{"named_a.b": None, "positional_0": {"child": "value"}}], schema=schema) + + +def test_arguments_preserve_metadata_and_wire_layout() -> None: + original = _arguments() + args = Arguments.from_batch(original) + assert args.to_batch().equals(original, check_metadata=True) + payload = args.serialize_to_bytes() + wire = pa.ipc.open_stream(payload).read_next_batch() + assert wire.schema.names == ["args"] + assert wire.num_rows == 1 + assert list(wire.schema.field("args").type) == list(original.schema) + restored = Arguments.deserialize_from_bytes(payload) + assert restored.to_batch().equals(original, check_metadata=True) + assert restored.get("a.b", default=42) is None + assert restored.get("absent", default=42) == 42 + assert restored.get(0) == {"child": "value"} + + +def test_struct_decode_preserves_field_metadata() -> None: + original = _arguments() + restored = Arguments.decode(original.to_struct_array()[0]) + # Schema metadata lives outside a struct scalar; field metadata is inside. + assert restored.schema().equals(original.schema.remove_metadata(), check_metadata=True) + + +def test_explicit_schema_preserves_metadata_for_scalar_callers() -> None: + schema = pa.schema([pa.field("positional_0", pa.int64(), metadata={b"meaning": b"counter"})]) + args = Arguments(positional=(pa.scalar(42),), argument_schema=schema) + assert Arguments.deserialize_from_bytes(args.serialize_to_bytes()).schema().equals(schema, check_metadata=True) + + +def test_replaced_values_must_match_retained_schema() -> None: + args = Arguments.from_batch(_arguments()) + with pytest.raises(ValueError, match="declared type"): + replace(args, named={"a.b": pa.scalar("different type")}).serialize_to_bytes() + with pytest.raises(ValueError, match="exactly the supplied"): + replace(args, named={"renamed": pa.scalar(None, type=pa.int32())}).serialize_to_bytes() + + +def test_empty_arguments_preserve_metadata() -> None: + batch = TypedValueBatch.empty(metadata={b"context": b"no arguments"}).batch + restored = Arguments.deserialize_from_bytes(Arguments.from_batch(batch).serialize_to_bytes()) + assert restored.to_batch().equals(batch, check_metadata=True) + assert restored.positional == () + assert restored.named is None + + +def test_bind_and_init_preserve_argument_metadata() -> None: + original = _arguments() + bind = BindRequest(function_name="f", arguments=Arguments.from_batch(original), function_type=FunctionType.TABLE) + restored = BindRequest.deserialize_from_bytes(bind.serialize_to_bytes()) + assert restored.arguments.to_batch().equals(original, check_metadata=True) + init = InitRequest(bind_call=restored, output_schema=pa.schema([])) + restored_init = InitRequest.deserialize_from_bytes(init.serialize_to_bytes()) + assert restored_init.bind_call.arguments.to_batch().equals(original, check_metadata=True) + + +@pytest.mark.parametrize( + "array", + [ + pa.array(["a"]).dictionary_encode(), + pa.ExtensionArray.from_storage(pa.bool8(), pa.array([1], type=pa.int8())), + pa.RunEndEncodedArray.from_arrays(pa.array([1], type=pa.int16()), pa.array([42])), + pa.UnionArray.from_dense(pa.array([0], type=pa.int8()), pa.array([0], type=pa.int32()), [pa.array([42])]), + pa.array([[("a", 1)]], type=pa.map_(pa.string(), pa.int64())), + ], +) +def test_encoded_arrow_types_retain_values_and_annotations(array: pa.Array[pa.Scalar[pa.DataType]]) -> None: + schema = pa.schema([pa.field("positional_0", array.type, metadata={b"annotation": b"preserved"})]) + batch = pa.RecordBatch.from_arrays([array], schema=schema) + values = TypedValueBatch.deserialize_from_bytes(TypedValueBatch(batch).serialize_to_bytes()) + assert values.batch.equals(batch, check_metadata=True) + args = Arguments.deserialize_from_bytes(Arguments.from_batch(batch).serialize_to_bytes()) + assert args.to_batch().equals(batch, check_metadata=True) + + +@pytest.mark.parametrize( + "name", ["positional_-1", "positional_01", "positional_1", "positional_99999999999999999999", "named_", "unknown"] +) +def test_malformed_argument_names_reject(name: str) -> None: + with pytest.raises(ValueError, match="invalid|contiguous"): + Arguments.from_batch(pa.record_batch({name: [1]})) + + +@pytest.mark.parametrize( + "batch", + [ + pa.record_batch({"wrong": [1]}), + pa.record_batch({"args": [1]}), + pa.record_batch({"args": pa.array([None], type=pa.struct([pa.field("positional_0", pa.int64())]))}), + ], +) +def test_malformed_argument_envelope_rejects(batch: pa.RecordBatch) -> None: + with pytest.raises(ValueError, match="args struct|present struct"): + Arguments.deserialize_from_bytes(TypedValueBatch(batch).serialize_to_bytes()) + + +def test_duckdb_15_null_empty_struct_means_no_arguments() -> None: + """Native 1.5 table-input functions send this form for their empty constants.""" + batch = pa.record_batch({"args": pa.array([None], type=pa.struct([]))}) + restored = Arguments.deserialize_from_bytes(TypedValueBatch(batch).serialize_to_bytes()) + assert restored.positional == () + assert restored.named is None + assert Arguments.decode(batch.column(0)[0]) == Arguments() + + +class _MetadataWorker(Worker): + """Return the decoded argument schema through the real bind RPC.""" + + def bind(self, request: BindRequest, ctx: CallContext) -> BindResponse: + return BindResponse(output_schema=request.arguments.schema()) + + +def test_public_http_bind_preserves_argument_metadata() -> None: + original = _arguments() + app = create_app(_MetadataWorker, describe=False, signing_key=b"metadata-test-key") + with Client.from_http("http://vgi.test", httpx_client=_SyncTestClient(app)) as client: + response = client.bind(function_name="metadata", schema_path=["main"], arguments=Arguments.from_batch(original)) + assert response.output_schema.equals(original.schema, check_metadata=True) diff --git a/tests/test_catalog_contract_fixtures.py b/tests/test_catalog_contract_fixtures.py new file mode 100644 index 00000000..0f5f2cb7 --- /dev/null +++ b/tests/test_catalog_contract_fixtures.py @@ -0,0 +1,314 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""The corpus distributed to every SDK retains semantic metadata and raw paths.""" + +import base64 +import json +from pathlib import Path + +import pyarrow as pa +import pytest + +from vgi.catalog.capabilities import SCHEMA_OPTIONS_CREATE, SCHEMA_OPTIONS_READ, CatalogCapabilities +from vgi.catalog.catalog_interface import CatalogAttachResult, MacroInfo, TableInfo, ViewInfo +from vgi.catalog.column_metadata import ColumnMetadata, column_metadata, validate_column_metadata +from vgi.catalog.dependencies import DependencySnapshot, ObjectDependency +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.indexes import IndexDefinition, IndexKey +from vgi.catalog.layout import LayoutChangeMode, SortKey, SqlExpression, TableLayout, TableLayoutChange +from vgi.catalog.macros import MacroOverload, MacroParameter +from vgi.catalog.options import OptionChanges, OptionDefinitions, OptionState +from vgi.catalog.sql import column_expressions +from vgi.codegen.catalog_contract_fixtures import attach_records, constraint_records, render +from vgi.column_path import RequiredFilterPaths +from vgi.logical_types import LOGICAL_TYPE_PROFILES, validate_logical_values +from vgi.protocol import MacroCreateRequest, TableCreateRequest +from vgi.query_parameters import QueryParameters +from vgi.typed_values import TypedValueBatch +from vgi.worker import Worker + + +def test_attach_vectors_preserve_default_schema_absence_and_raw_names() -> None: + fixtures = {name: base64.b64decode(value) for name, value in json.loads(render())["records"].items()} + assert CatalogAttachResult.ARROW_SCHEMA.field("default_schema").nullable + for name, source in attach_records().items(): + restored = CatalogAttachResult.deserialize_from_bytes(fixtures[name]) + assert restored.default_schema == source.default_schema + assert restored == source + assert restored.serialize_to_bytes() == fixtures[name] + + +def test_column_metadata_schema_vectors_preserve_literals_and_reject_invalid_values() -> None: + fixtures = json.loads(render())["column_metadata_schemas"] + restored = {} + for name, payload in fixtures["valid"].items(): + schema = pa.ipc.read_schema(pa.py_buffer(base64.b64decode(payload))) + validate_column_metadata(schema) + assert pa.ipc.read_schema(schema.serialize()).equals(schema, check_metadata=True) + restored[name] = schema + assert column_metadata(restored["absent"].field(0)) == ColumnMetadata() + assert column_metadata(restored["empty"].field(0)) == ColumnMetadata(comment="", tags={}) + assert column_metadata(restored["literal"].field(0)) == ColumnMetadata( + comment="A café column\x00 with a literal dot.", tags={"a.b": 'literal "value"', "": "", "雪": "\x00"} + ) + assert restored["literal"].metadata == {b"provider": b"retained"} + metadata = restored["empty"].field(0).metadata + assert metadata is not None + assert metadata[b"provider.note"] == b"keep me" + for payload in fixtures["invalid"].values(): + schema = pa.ipc.read_schema(pa.py_buffer(base64.b64decode(payload))) + with pytest.raises(ValueError, match="."): + validate_column_metadata(schema) + + +def test_canonical_constraint_vectors_preserve_every_field() -> None: + fixtures = {name: base64.b64decode(value) for name, value in json.loads(render())["records"].items()} + for name, source in constraint_records().items(): + decoded = type(source).deserialize_from_bytes(fixtures[name]) + assert decoded == source + assert decoded.serialize_to_bytes() == fixtures[name] + + +def test_generated_schema_vectors_validate_without_a_capability_offer() -> None: + fixtures = json.loads(render())["generated_column_schemas"] + owner = fixtures["owner_schema_path"] + for payload in fixtures["valid"].values(): + schema = pa.ipc.read_schema(pa.py_buffer(base64.b64decode(payload))) + before = schema.serialize().to_pybytes() + Worker._require_column_expression_profiles(schema, CatalogCapabilities(), "read generated columns", owner) + assert schema.serialize().to_pybytes() == before + for field in schema: + generated = column_expressions(field) + if generated.generated is not None: + assert generated.generation == "virtual" + assert generated.generated.schema_path == owner + for name, payload in fixtures["invalid"].items(): + schema = pa.ipc.read_schema(pa.py_buffer(base64.b64decode(payload))) + with pytest.raises((ValueError, TypeError, NotImplementedError, pa.ArrowInvalid, pa.ArrowIOError)) as error: + Worker._require_column_expression_profiles(schema, CatalogCapabilities(), "read generated columns", owner) + assert str(error.value), name + + +def test_index_vectors_preserve_keys_typed_options_and_association() -> None: + fixtures = {name: base64.b64decode(value) for name, value in json.loads(render())["records"].items()} + definition = IndexDefinition.deserialize_from_bytes(fixtures["index_definition"]) + assert definition.keys == [fixtures["index_key_column"], fixtures["index_key_expression"]] + assert definition.options.serialize_to_bytes() == fixtures["values"] + assert IndexKey.deserialize_from_bytes(fixtures["index_key_column"]).column_path == ["a.b"] + assert IndexKey.deserialize_from_bytes(fixtures["index_key_nested"]).column_path == ["record", "a.b"] + primary = IndexDefinition.deserialize_from_bytes(fixtures["index_definition_primary"]) + assert primary.associated_constraint is not None + assert primary.associated_constraint.owner_table == definition.table.name + + +def test_dependency_vectors_preserve_alter_flags_and_column_identity() -> None: + fixtures = {name: base64.b64decode(value) for name, value in json.loads(render())["records"].items()} + membership = ObjectDependency.deserialize_from_bytes(fixtures["dependency_membership"]) + assert membership.blocks_alter is False + assert membership.subject_column_paths is None and membership.dependent_column_path is None + literal = ObjectDependency.deserialize_from_bytes(fixtures["dependency_column_literal"]) + nested = ObjectDependency.deserialize_from_bytes(fixtures["dependency_column_nested"]) + assert literal.blocks_alter is True and literal.subject_column_paths == [["a.b"]] + assert nested.subject_column_paths == [["record", "a.b"]] + generated = ObjectDependency.deserialize_from_bytes(fixtures["dependency_generated"]) + assert generated.dependent == generated.subject + assert generated.dependent_column_path == ["generated"] + assert generated.subject_column_paths == [["a.b"], ["record", "a.b"]] + chain = ObjectDependency.deserialize_from_bytes(fixtures["dependency_generated_chain"]) + assert chain.dependent_column_path == ["second.generated"] + assert chain.subject_column_paths == [["generated"]] + snapshot = DependencySnapshot.deserialize_from_bytes(fixtures["dependency_column_snapshot"]) + assert snapshot.dependencies == [ + fixtures["dependency_column_literal"], + fixtures["dependency_generated"], + fixtures["dependency_generated_chain"], + ] + assert snapshot.serialize_to_bytes() == fixtures["dependency_column_snapshot"] + + +def test_query_parameter_corpus_preserves_mapping_types_metadata_and_empty_row() -> None: + fixtures = json.loads(render())["records"] + for name in ("query_parameters", "query_empty_parameters"): + source = QueryParameters.deserialize_from_bytes(base64.b64decode(fixtures[name])) + restored = QueryParameters.deserialize_from_bytes(source.serialize_to_bytes()) + assert restored.batch.equals(source.batch, check_metadata=True) + assert restored.batch.num_rows == 1 + assert restored.mode == ("positional" if name == "query_empty_parameters" else "named") + if name == "query_empty_parameters": + assert restored.batch.num_columns == 0 + else: + assert not restored.batch.column("optional")[0].is_valid + assert restored.schema.field("record").type[0].metadata == {b"nested": b"retained"} + + +def test_column_requests_preserve_raw_paths_and_definitions() -> None: + fixtures = json.loads(render())["records"] + names = [ + name + for name in fixtures + if name.startswith(("column_", "not_null_")) and name.endswith(("_literal", "_nested")) + ] + assert len(names) == 18 + for name in names: + with pa.ipc.open_stream(base64.b64decode(fixtures[name])) as reader: + (row,) = reader.read_all().to_pylist() + nested = name.endswith("_nested") + assert row["column_path"] == (["a", 'b."quoted'] if nested else ["a.b"]) + assert row["transaction_opaque_data"] == (b"transaction" if nested else None) + if "column_definition" in row: + definition = pa.ipc.read_schema(pa.py_buffer(row["column_definition"])) + assert definition.field(0).name == row["column_path"][-1] + if name.startswith("column_add_"): + expression = column_expressions(definition.field(0)).default + assert expression is not None and expression.sql == "7" + assert expression.schema_path == row["schema_path"] + assert expression.evaluation_context == QueryEvaluationContext() + metadata = definition.field(0).metadata + assert metadata is not None + assert b"default" not in metadata + if "expression" in row: + if name == "column_type_change_literal": + assert row["expression"] is None + else: + assert isinstance(row["expression"], bytes) + expression = SqlExpression.deserialize_from_bytes(row["expression"]) + assert expression.sql == "7" and expression.schema_path == row["schema_path"] + assert expression.evaluation_context == QueryEvaluationContext() + if "comment" in row: + assert row["comment"] == ("kept" if nested else None) + + +def test_checked_in_corpus_and_contents() -> None: + path = Path(__file__).resolve().parents[1] / "conformance/catalog-contracts.json" + assert path.read_text() == render() + fixtures = {name: base64.b64decode(value) for name, value in json.loads(path.read_text())["records"].items()} + capabilities = CatalogCapabilities.deserialize_from_bytes(fixtures["capabilities"]) + assert capabilities.read == [SCHEMA_OPTIONS_READ] + assert capabilities.ddl == [SCHEMA_OPTIONS_CREATE] + definitions = OptionDefinitions.deserialize_from_bytes(fixtures["definitions"]) + assert definitions.create_names == ["optional", "record"] + assert definitions.alter_names == ["optional"] + values = TypedValueBatch.deserialize_from_bytes(fixtures["values"]) + assert definitions.schema.equals(values.schema, check_metadata=True) + assert definitions.defaults.batch.equals(values.batch, check_metadata=True) + assert values.schema.metadata == {b"provider": b"catalog-contracts/v1"} + assert values.schema.field("record").type[0].metadata == {b"nested": b"retained"} + assert not values.batch.column("optional")[0].is_valid + changes = OptionChanges.deserialize_from_bytes(fixtures["changes"]) + assert changes.set_values is not None + assert changes.set_values.batch.equals(values.batch, check_metadata=True) + assert changes.reset_names == ["reset.me"] + assert OptionChanges.deserialize_from_bytes(fixtures["reset_only"]).set_values is None + state = OptionState.deserialize_from_bytes(fixtures["state"]) + assert state.explicit_names == ["optional"] + assert state.values.batch.equals(values.batch, check_metadata=True) + assert OptionState.deserialize_from_bytes(fixtures["empty_state"]).values.batch.num_rows == 1 + paths = RequiredFilterPaths.deserialize_from_bytes(fixtures["paths"]) + assert paths.groups == [[["a.b"], ["a", "b"]], [["record", 'literal."name']]] + assert RequiredFilterPaths.deserialize_from_bytes(fixtures["empty_paths"]).groups == [] + + +def test_layout_corpus_preserves_nested_keys_and_nullable_changes() -> None: + fixtures = {name: base64.b64decode(value) for name, value in json.loads(render())["records"].items()} + expression = SqlExpression.deserialize_from_bytes(fixtures["expression"]) + assert expression.schema_path == ["one.two", "nested"] + key = SortKey.deserialize_from_bytes(fixtures["sort_key"]) + assert key.expression == expression and key.direction == "desc" and key.null_order == "first" + layout = TableLayout.deserialize_from_bytes(fixtures["layout"]) + assert layout.partition_expressions() == [expression] and layout.sort_keys() == [key] + change = TableLayoutChange.deserialize_from_bytes(fixtures["layout_change"]) + assert change.mode == LayoutChangeMode.REWRITE and change.partition_by == [] + clear = TableLayoutChange.deserialize_from_bytes(fixtures["layout_clear"]) + assert clear.partition_by is None and clear.sorted_by == [] + request = TableCreateRequest.deserialize_from_bytes(fixtures["table_create"]) + assert request.layout == layout and request.options is not None + assert request.options.serialize_to_bytes() == fixtures["values"] + old = TableCreateRequest.deserialize_from_bytes(fixtures["table_create_legacy"]) + assert old.options is None and old.layout is None and old.comment is None and old.tags is None + info = TableInfo.deserialize_from_bytes(fixtures["table_info"]) + assert info.layout == layout and info.options is not None + assert info.options.serialize_to_bytes() == fixtures["state"] + assert info.definition_revision == b"revision\0\xff" + assert info.constraint_infos()[0].definition.serialize_to_bytes() == request.constraint_definitions[0] + assert info.constraint_infos()[0].ref.name == "fixture-stable-key" + + +def test_views_preserve_ordered_raw_aliases() -> None: + fixtures = json.loads(render())["records"] + for name, aliases in (("view_info", ["a.b", 'with"quote']), ("view_info_implicit", [])): + info = ViewInfo.deserialize_from_bytes(base64.b64decode(fixtures[name])) + assert info.aliases == aliases + restored = ViewInfo.deserialize_from_bytes(info.serialize_to_bytes()) + assert restored == info + + +def test_views_preserve_security_modes() -> None: + fixtures = json.loads(render())["records"] + for name, security in (("view_info", "regular"), ("view_info_barrier", "barrier")): + info = ViewInfo.deserialize_from_bytes(base64.b64decode(fixtures[name])) + assert info.security == security + assert ViewInfo.deserialize_from_bytes(info.serialize_to_bytes()) == info + + +def test_logical_type_vectors_preserve_tuple_null_and_absence() -> None: + fixtures = {name: base64.b64decode(value) for name, value in json.loads(render())["records"].items()} + vectors = { + name: TypedValueBatch.deserialize_from_bytes(payload) + for name, payload in fixtures.items() + if name.startswith("logical_") + } + for values in vectors.values(): + validate_logical_values(values, selected_profiles=LOGICAL_TYPE_PROFILES) + assert TypedValueBatch.deserialize_from_bytes(values.serialize_to_bytes()).batch.equals( + values.batch, check_metadata=True + ) + assert not vectors["logical_tuple"].schema.equals(vectors["logical_struct"].schema, check_metadata=True) + assert not vectors["logical_variant_sql_null"].batch.column(0)[0].is_valid + expected = { + "logical_variant_null": ("010000", "00"), + "logical_variant_absent": ("010000", "020000"), + "logical_variant_null_member": ("0101000770726573656e74", "020100000100"), + } + for name, (metadata, value) in expected.items(): + field = vectors[name].schema.field(0) + assert field.metadata == { + b"ARROW:extension:name": b"arrow.parquet.variant", + b"ARROW:extension:metadata": b"", + } + assert field.type == pa.struct( + [pa.field("metadata", pa.binary(), nullable=False), pa.field("value", pa.binary())] + ) + assert vectors[name].batch.column(0)[0].as_py() == { + "metadata": bytes.fromhex(metadata), + "value": bytes.fromhex(value), + } + + +def test_macro_vectors_preserve_single_and_multiple_overload_families() -> None: + fixtures = {name: base64.b64decode(value) for name, value in json.loads(render())["records"].items()} + parameter = MacroParameter.deserialize_from_bytes(fixtures["macro_parameter"]) + assert parameter.declared_type is not None and parameter.declared_type.field(0).type == pa.int64() + assert parameter.default_value is not None and not parameter.default_value.batch.column(0)[0].is_valid + expression = MacroParameter.deserialize_from_bytes(fixtures["macro_parameter_expression"]) + assert expression.default_expression is not None and expression.default_expression.sql == "2 + 3" + assert MacroParameter.deserialize_from_bytes(fixtures["macro_parameter_untyped"]).declared_type is None + overload = MacroOverload.deserialize_from_bytes(fixtures["macro_overload"]) + assert [parameter.name for parameter in overload.parameters] == ["x", "fallback", "scale"] + create = MacroCreateRequest.deserialize_from_bytes(fixtures["macro_create"]) + info = MacroInfo.deserialize_from_bytes(fixtures["macro_info"]) + assert len(create.overloads) == 2 + assert len(info.overloads) == 2 + resolved = info.overloads[0].parameters[2] + assert resolved.default_expression is None + assert resolved.default_value is not None and resolved.default_value.batch.column(0)[0].as_py() == 5 + single_create = MacroCreateRequest.deserialize_from_bytes(fixtures["macro_create_single"]) + single_info = MacroInfo.deserialize_from_bytes(fixtures["macro_info_single"]) + assert single_create.overloads == single_info.overloads + assert len(single_info.overloads) == 1 + assert single_info.overloads[0].definition == "x + 1" + for record_type in (MacroCreateRequest, MacroInfo): + assert not record_type.ARROW_SCHEMA.field("overloads").nullable + assert not {"parameters", "definition", "parameter_default_values", "arguments_schema"}.intersection( + record_type.ARROW_SCHEMA.names + ) diff --git a/tests/test_client_reflection.py b/tests/test_client_reflection.py index 8006360e..f632adec 100644 --- a/tests/test_client_reflection.py +++ b/tests/test_client_reflection.py @@ -27,7 +27,7 @@ _SECONDARY = "conformance.Secondary.v1" _REFLECTION = "vgi_rpc.Reflection.v1" -_EXPECTED_NAMES = ["vgi.v2", _SECONDARY, _REFLECTION] +_EXPECTED_NAMES = ["vgi.v3", _SECONDARY, _REFLECTION] _HASH = re.compile(r"^[0-9a-f]{64}$") @@ -40,7 +40,7 @@ def _server_view() -> list[HostedProtocol]: def _assert_fixture_listing(listing: list[HostedProtocol]) -> None: - """The fixture's hosted set: ``vgi.v2`` first, the secondary, then reflection.""" + """The fixture's hosted set: ``vgi.v3`` first, the secondary, then reflection.""" assert all(isinstance(p, HostedProtocol) for p in listing) assert [p.name for p in listing] == _EXPECTED_NAMES assert listing[0].version == VgiProtocol.protocol_version @@ -62,7 +62,7 @@ def test_stdio_direct(self) -> None: with Client(SUBPROCESS_FIXTURE_WORKER, pool=None) as client: first = client.list_protocols() _assert_fixture_listing(first) - # The pipe is still usable for vgi.v2 after a reflection call. + # The pipe is still usable for vgi.v3 after a reflection call. assert client.list_protocols() == first def test_http(self, _shared_http_base_url: Callable[[], str]) -> None: @@ -89,10 +89,10 @@ def test_describes_secondary(self, client_transport: Callable[[], Client]) -> No assert described.protocol_hash == listed[_SECONDARY].hash assert "echo_string" in described.methods - def test_describes_vgi_v2(self) -> None: - """``vgi.v2`` describes with its version and the client's own method set.""" + def test_describes_vgi_v3(self) -> None: + """``vgi.v3`` describes with its version and the client's own method set.""" with Client(SUBPROCESS_FIXTURE_WORKER, pool=None) as client: - described = client.describe_protocol("vgi.v2") + described = client.describe_protocol("vgi.v3") assert described.protocol_version == VgiProtocol.protocol_version assert {"bind", "init"} <= set(described.methods) @@ -115,7 +115,7 @@ def _fail(target: object) -> Any: class TestPreReflectionWorker: - """A worker without reflection is reported as hosting only ``vgi.v2``. + """A worker without reflection is reported as hosting only ``vgi.v3``. Which server answers mean "no reflection" is vgi-rpc's call (it raises ``ReflectionNotSupportedError``, tested there); the client's job is only @@ -123,8 +123,8 @@ class TestPreReflectionWorker: ``ClientError``. """ - def test_falls_back_to_vgi_v2(self, monkeypatch: pytest.MonkeyPatch) -> None: - """``ReflectionNotSupportedError`` yields one ``vgi.v2`` entry with an empty hash.""" + def test_falls_back_to_vgi_v3(self, monkeypatch: pytest.MonkeyPatch) -> None: + """``ReflectionNotSupportedError`` yields one ``vgi.v3`` entry with an empty hash.""" error = ReflectionNotSupportedError( "ProtocolNotSupportedError", "not hosted", @@ -134,7 +134,7 @@ def test_falls_back_to_vgi_v2(self, monkeypatch: pytest.MonkeyPatch) -> None: ) _patch_listing(monkeypatch, error) with Client(SUBPROCESS_FIXTURE_WORKER, pool=None) as client: - assert client.list_protocols() == [HostedProtocol(name="vgi.v2", version="", hash="")] + assert client.list_protocols() == [HostedProtocol(name="vgi.v3", version="", hash="")] def test_other_errors_raise(self, monkeypatch: pytest.MonkeyPatch) -> None: """Any other reflection failure is a ``ClientError``, not a silent fallback.""" diff --git a/tests/test_codegen_request_inventory.py b/tests/test_codegen_request_inventory.py index 7a144fd8..080949cc 100644 --- a/tests/test_codegen_request_inventory.py +++ b/tests/test_codegen_request_inventory.py @@ -7,7 +7,11 @@ import inspect import typing -from vgi.codegen._common import REQUEST_TYPES +from vgi.codegen._common import EXTRA_RESPONSE_TYPES, REQUEST_TYPES, SHARED_CONTRACT_TYPES +from vgi.codegen.go_types import GO_TYPE_RECORDS +from vgi.codegen.java_types import JAVA_RECORDS +from vgi.codegen.rust_types import RUST_TYPES +from vgi.codegen.ts_types import TS_RECORD_TYPES from vgi.protocol import VgiProtocol @@ -21,3 +25,11 @@ def test_every_request_dataclass_is_in_codegen_inventory() -> None: missing[method_name] = request_type.__name__ assert not missing, f"Opaque request dataclasses missing from REQUEST_TYPES: {missing}" + + +def test_shared_contract_records_have_schemas_and_all_typed_models() -> None: + """Preparing common types must not leave a worker SDK with only raw bytes.""" + required = set(SHARED_CONTRACT_TYPES) + assert required <= set(EXTRA_RESPONSE_TYPES) # C++ schemas and C# models use this inventory. + for records in (GO_TYPE_RECORDS, JAVA_RECORDS, RUST_TYPES, TS_RECORD_TYPES): + assert required <= set(records) diff --git a/tests/test_column_path.py b/tests/test_column_path.py new file mode 100644 index 00000000..41033231 --- /dev/null +++ b/tests/test_column_path.py @@ -0,0 +1,135 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Structural identities, required-filter semantics and explicit legacy migration.""" + +from collections.abc import Callable +from typing import Any, cast + +import pyarrow as pa +import pytest + +from vgi.column_path import RequiredFilterPaths, column_definition, column_path_key, resolve_column_path + + +def _schema() -> pa.Schema: + return pa.schema( + [ + pa.field("a.b", pa.string()), + pa.field("a", pa.struct([pa.field("b", pa.int64()), pa.field('c."d', pa.bool_())])), + pa.field("other", pa.int32()), + ] + ) + + +def test_literal_dotted_names_and_nested_names_are_distinct() -> None: + assert column_path_key(["a.b"]) != column_path_key(["a", "b"]) + assert column_path_key(["A"]) != column_path_key(["a"]) + assert column_path_key(["İ"]) != column_path_key(["i\u0307"]) + assert resolve_column_path(_schema(), ["a.b"]).type == pa.string() + assert resolve_column_path(_schema(), ["a", "b"]).type == pa.int64() + assert resolve_column_path(_schema(), ["a", 'c."d']).type == pa.bool_() + + +@pytest.mark.parametrize("path", ["a.b", b"a", [1], None, {"a"}]) +def test_invalid_component_types_reject(path: Any) -> None: + with pytest.raises(TypeError, match="column path"): + column_path_key(path) + + +@pytest.mark.parametrize("path", [[], [""], ["a", ""], ["a\x00b"]]) +def test_invalid_components_reject(path: list[str]) -> None: + with pytest.raises(ValueError, match="column path"): + column_path_key(path) + + +@pytest.mark.parametrize("path", [["missing"], ["a", "missing"], ["other", "x"], ["A"]]) +def test_resolution_rejects_missing_and_nonstruct_parents(path: list[str]) -> None: + with pytest.raises(ValueError, match="missing or ambiguous|non-struct"): + resolve_column_path(_schema(), path) + + +def test_duplicate_arrow_names_are_ambiguous() -> None: + schema = pa.schema([pa.field("x", pa.int32()), pa.field("x", pa.int64())]) + with pytest.raises(ValueError, match="ambiguous"): + resolve_column_path(schema, ["x"]) + + +def test_and_of_or_groups_and_component_prefixes() -> None: + required = RequiredFilterPaths(groups=[[["a", "b"], ["other"]], [["a.b"]]]) + required.validate_schema(_schema()) + assert required.is_satisfied_by([["other"], ["a.b"]]) + assert required.is_satisfied_by([["a"], ["a.b"]]) + assert not required.is_satisfied_by([["a.b"]]) + assert not required.is_satisfied_by([["a", "b"]]) + assert not required.is_satisfied_by([["a", "c"], ["a.b"]]) + assert not RequiredFilterPaths(groups=[[["a"]]]).is_satisfied_by([["a", "b"]]) + assert RequiredFilterPaths().is_satisfied_by([]) + + +def test_wire_round_trip_retains_groups_and_exact_components() -> None: + required = RequiredFilterPaths(groups=[[["a", 'c."d'], ["a.b"]], [["other"]]]) + restored = RequiredFilterPaths.deserialize_from_bytes(required.serialize_to_bytes()) + assert restored == required + assert RequiredFilterPaths.ARROW_SCHEMA.field("groups").type == pa.list_(pa.list_(pa.list_(pa.string()))) + + +@pytest.mark.parametrize("groups", [[[]], [[["a"], ["a"]]], [[[]]], [[[""]]]]) +def test_malformed_groups_reject(groups: Any) -> None: + with pytest.raises(ValueError): + RequiredFilterPaths(groups=groups) + + +def test_requirements_must_resolve_before_using_them() -> None: + required = RequiredFilterPaths(groups=[[["a", "missing"]]]) + with pytest.raises(ValueError, match="missing or ambiguous"): + required.validate_schema(_schema()) + + +def test_legacy_migration_requires_unambiguous_schema_evidence() -> None: + with pytest.raises(ValueError, match="ambiguous; use raw components"): + RequiredFilterPaths.from_legacy([["a.b"]], schema=_schema()) + with pytest.raises(ValueError, match="missing or ambiguous"): + RequiredFilterPaths.from_legacy([["missing"]], schema=_schema()) + nested = pa.schema([_schema().field("a")]) + assert RequiredFilterPaths.from_legacy([["a.b"]], schema=nested).groups == [[["a", "b"]]] + literal = pa.schema([_schema().field("a.b")]) + assert RequiredFilterPaths.from_legacy([["a.b"]], schema=literal).groups == [[["a.b"]]] + + +def test_mutation_cannot_bypass_validation() -> None: + required = RequiredFilterPaths() + required.groups.append([]) + with pytest.raises(ValueError, match="must not be empty"): + required.is_satisfied_by([["a"]]) + + +@pytest.mark.parametrize( + "attribute", + [ + b"vgi.column.default.v1", + b"vgi.column.generated.v1", + b"vgi.column.generation.v1", + b"vgi.column.default.v2", + b"vgi.column.generated.v2", + b"vgi.column.generation.v2", + ], +) +@pytest.mark.parametrize("wrapper", ["struct", "list", "dictionary"]) +def test_type_carrier_rejects_nested_expression_metadata(attribute: bytes, wrapper: str) -> None: + child = pa.field("child", pa.int32(), metadata={attribute: b"not a type annotation"}) + dtype: pa.DataType = pa.list_(child) if wrapper == "list" else pa.struct([child]) + if wrapper == "dictionary": + # Nested dictionary values are valid at runtime but omitted by PyArrow stubs. + dictionary = cast(Callable[[pa.DataType, pa.DataType], pa.DataType], pa.dictionary) + dtype = dictionary(pa.int16(), dtype) + payload = pa.schema([pa.field("target", dtype)]).serialize().to_pybytes() + with pytest.raises(ValueError, match="only the target type"): + column_definition(payload, ["target"], type_only=True) + + +def test_type_carrier_retains_nested_type_annotations() -> None: + child = pa.field("child", pa.int32(), metadata={b"provider.type": b"retained"}) + field = pa.field("target", pa.struct([child]), metadata={b"provider.type": b"outer"}) + payload = pa.schema([field]).serialize().to_pybytes() + assert column_definition(payload, ["target"], type_only=True).equals(field, check_metadata=True) diff --git a/tests/test_documentation_examples.py b/tests/test_documentation_examples.py index ac36a7b3..d31ae197 100644 --- a/tests/test_documentation_examples.py +++ b/tests/test_documentation_examples.py @@ -88,6 +88,7 @@ "docs/generator-api.md", "docs/catalog-interface.md", "docs/argument-serialization.md", + "docs/protocol/shared-catalog-values.md", "docs/lifecycle.md", "docs/metadata.md", "docs/cli.md", diff --git a/tests/test_duckdb2_manifest.py b/tests/test_duckdb2_manifest.py new file mode 100644 index 00000000..92bd077f --- /dev/null +++ b/tests/test_duckdb2_manifest.py @@ -0,0 +1,153 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""The release inventory must cover the actual audit, not just count its rows.""" + +import copy +import hashlib +import json +from pathlib import Path +from typing import NotRequired, TypedDict + +import pytest + +from scripts.check_duckdb2_manifest import audit_rows, check_inventory, pending_decisions + + +class _Area(TypedDict): + """One mutable review area in the inventory fixture.""" + + title: str + scope: list[str] + records: list[str] + existing_methods: list[str] + disposition: str + capability_contract: str + reason: NotRequired[str] + later_release_cost: NotRequired[str] + + +class _Manifest(TypedDict): + """Precisely typed inventory sections mutated by the negative cases.""" + + format_version: int + sources: dict[str, str] + areas: dict[str, _Area] + audit_fields: list[dict[str, str | int | list[str]]] + proposals: list[object] + + +_ROOT = Path(__file__).resolve().parents[1] + + +def test_current_manifest_matches_native_audit() -> None: + native = _ROOT.parent / "vgi" + if not native.is_dir(): + pytest.skip("native checkout required for field-audit comparison") + manifest = json.loads((_ROOT / "docs/protocol/duckdb-2.0-manifest.json").read_text()) + assert check_inventory(dict(manifest), native) == [] + + +@pytest.fixture +def inventory(tmp_path: Path) -> tuple[_Manifest, Path]: + """A minimal external audit with two distinct requirements.""" + source = ( + "## Shared creation fields and object identity\n\n" + "| Field | Action |\n| --- | --- |\n| name | preserve path |\n| comment | preserve NULL |\n" + "## Runtime evidence and limits\n| Probe | Result |\n| --- | --- |\n| sample | pass |\n" + ) + (tmp_path / "audit.md").write_text(source) + (tmp_path / "plan.md").write_text( + "### Target contract package\n" + "| Area | Scope |\n| --- | --- |\n| Identity | preserve identity |\n" + "### Additional candidates to resolve before freeze\n" + "| Candidate | Decision |\n| --- | --- |\n| Queries | review query execution |\n" + ) + rows = audit_rows(source) + manifest: _Manifest = { + "format_version": 1, + "sources": { + "audit_path": "audit.md", + "audit_sha256": hashlib.sha256(source.encode()).hexdigest(), + "plan_path": "plan.md", + }, + "areas": { + "identity": { + "title": "Identity", + "scope": ["preserve identity"], + "records": ["CatalogObjectRef"], + "existing_methods": ["catalog_attach"], + "disposition": "include", + "capability_contract": "existing identity contract", + }, + "query": { + "title": "Queries", + "scope": ["review query execution"], + "records": ["QueryRequest"], + "existing_methods": [], + "disposition": "include", + "capability_contract": "explicit query profile", + }, + }, + "audit_fields": [ + dict(row, id=str(index), area="identity", target=row["field"], disposition="include", status="reviewed") + for index, row in enumerate(rows) + ], + "proposals": [], + } + return manifest, tmp_path + + +@pytest.mark.parametrize("change", ["remove", "duplicate", "reorder", "alter_evidence", "nonexistent_method"]) +def test_inventory_rejects_lost_or_misrepresented_requirements(inventory: tuple[_Manifest, Path], change: str) -> None: + manifest, native = inventory + assert check_inventory(dict(manifest), native) == [] + changed = copy.deepcopy(manifest) + fields = changed["audit_fields"] + if change == "remove": + fields.pop() + elif change == "duplicate": + fields[1] = fields[0] + elif change == "reorder": + fields.reverse() + elif change == "alter_evidence": + fields[0]["audit_evidence"] = ["already implemented"] + else: + changed["areas"]["identity"]["existing_methods"] = ["catalog_nonexistent"] + assert check_inventory(dict(changed), native) + + +def test_missing_or_changed_audit_is_not_coverage(inventory: tuple[_Manifest, Path]) -> None: + manifest, native = inventory + (native / "audit.md").write_text((native / "audit.md").read_text().replace("preserve NULL", "new requirement")) + assert check_inventory(dict(manifest), native) + (native / "audit.md").unlink() + assert "required audit source is missing" in check_inventory(dict(manifest), native)[0] + + +def test_candidate_without_catalog_fields_cannot_disappear(inventory: tuple[_Manifest, Path]) -> None: + manifest, native = inventory + del manifest["areas"]["query"] + assert any("Queries: plan scope is missing" in error for error in check_inventory(dict(manifest), native)) + + +def test_changed_or_missing_plan_is_not_coverage(inventory: tuple[_Manifest, Path]) -> None: + manifest, native = inventory + (native / "plan.md").write_text( + (native / "plan.md").read_text().replace("preserve identity", "preserve raw identity") + ) + assert check_inventory(dict(manifest), native) + (native / "plan.md").unlink() + assert any("required plan source is missing" in error for error in check_inventory(dict(manifest), native)) + + +def test_unresolved_decision_cannot_be_hidden_by_inventory_coverage(inventory: tuple[_Manifest, Path]) -> None: + manifest, native = inventory + manifest["areas"]["identity"]["disposition"] = "decision_required" + assert check_inventory(dict(manifest), native) == [] + assert pending_decisions(dict(manifest)) + manifest["areas"]["identity"]["disposition"] = "defer" + assert pending_decisions(dict(manifest)) + manifest["areas"]["identity"]["reason"] = "missing engine callback" + manifest["areas"]["identity"]["later_release_cost"] = "new RPC and SDK version" + assert pending_decisions(dict(manifest)) == [] diff --git a/tests/test_generated_cpp_nanoarrow.py b/tests/test_generated_cpp_nanoarrow.py new file mode 100644 index 00000000..ed1c3319 --- /dev/null +++ b/tests/test_generated_cpp_nanoarrow.py @@ -0,0 +1,90 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +"""Both native C++ runtimes share the V3 model and wire shapes.""" + +from __future__ import annotations + +import io +import re +from typing import Protocol, TextIO + +import pyarrow as pa +import pytest + +from scripts import regen_generated +from vgi.codegen import cpp_records, cpp_request_builders, cpp_schemas, cpp_secret_request_builders, cpp_secret_schemas +from vgi.codegen._common import GeneratorError + + +class _SchemaEmitter(Protocol): + """The common backend-aware schema generator interface.""" + + def emit(self, out: TextIO, *, backend: str) -> None: + """Render a native backend without relying on untyped module attributes.""" + ... + + +@pytest.mark.parametrize("module", [cpp_schemas, cpp_request_builders, cpp_secret_schemas, cpp_secret_request_builders]) +def test_nanoarrow_inventory_matches_arrow(module: _SchemaEmitter) -> None: + """Switching runtimes preserves every schema or builder, including secret RPCs.""" + outputs = [] + for backend in ("arrow", "nanoarrow"): + out = io.StringIO() + module.emit(out, backend=backend) + outputs.append(out.getvalue()) + arrow, nano = outputs + assert set(re.findall(r"\b(\w+(?:Schema|Params))\(", arrow)) == set( + re.findall(r"\b(\w+(?:Schema|Params))\(", nano) + ) - {"MakeSchema"} + assert "arrow::" not in nano + assert "#include None: + """Every current record receives Nano codecs without changing the shared model.""" + out = io.StringIO() + cpp_records.emit(out, which, backend="nanoarrow") + source = out.getvalue() + for record in cpp_records.build_model().records.values(): + assert f"ToBatch(const {record.name} &value)" in source + assert "arrow::" not in source + assert "using LargeBytes = VgiBufferPtr" in source if which == "hpp" else "wire::CurrentBatchScope" in source + + +def test_nanoarrow_preserves_nested_nullability_and_dictionary_width() -> None: + """Nested list nullability and dictionary index width are wire-significant.""" + dtype = pa.struct( + [ + pa.field("paths", pa.list_(pa.field("item", pa.list_(pa.field("item", pa.string(), False)), False)), False), + pa.field("payload", pa.dictionary(pa.int32(), pa.binary()), True), + ] + ) + expr = cpp_schemas._na_emit_type(dtype, origin="test") + assert ( + "type::List(type::List(type::Utf8(), /*item_nullable=*/false), " + '/*item_nullable=*/false).Named("paths", /*nullable=*/false)' + ) in expr + assert 'type::Dictionary(type::Int32(), type::Binary()).Named("payload", /*nullable=*/true)' in expr + with pytest.raises(GeneratorError, match="ordered dictionary"): + cpp_schemas._na_emit_type(pa.dictionary(pa.int16(), pa.string(), ordered=True), origin="test") + + +def test_nanoarrow_v3_request_shapes() -> None: + """V3 nullable aliases and typed capability structs remain supported.""" + out = io.StringIO() + cpp_request_builders.emit(out, backend="nanoarrow") + source = out.getvalue() + assert "BuildOptionalStringListScalar(aliases)" in source + assert "const VgiColumn& query_pushdown" in source + assert "BuildStructScalar(query_pushdown, schema->field(6), true)" in source + + +def test_explicit_backend_is_limited_to_native_targets() -> None: + """Standalone C++ SDK rendering keeps its existing Arrow runtime.""" + native = regen_generated._render("vgi.codegen.cpp_schemas", "nanoarrow") + sdk = regen_generated._render("vgi.codegen.cpp_sdk:schemas") + assert "VgiSchemaPtr" in native + assert "arrow::Schema" in sdk + assert "VgiSchemaPtr" not in sdk diff --git a/tests/test_generated_cpp_request_builders.py b/tests/test_generated_cpp_request_builders.py index 9ab5fae3..34c4b07d 100644 --- a/tests/test_generated_cpp_request_builders.py +++ b/tests/test_generated_cpp_request_builders.py @@ -15,10 +15,12 @@ import io import os +import re from pathlib import Path import pytest +from vgi.codegen._registry import registry_methods from vgi.codegen.cpp_request_builders import emit @@ -40,6 +42,27 @@ def _vgi_generated_path() -> Path: ) +def test_native_runtime_registry_covers_protocol_requests_and_responses() -> None: + """A generated builder alone does not register a method with the native dispatcher.""" + path = _vgi_generated_path().parents[1] / "vgi_schema_registry.cpp" + if not path.exists(): + pytest.skip(f"native runtime registry not found at {path}") + source = path.read_text() + # The dispatcher now derives both contracts from the generated method table; + # only response policies (dynamic, opaque record, item lists) are handwritten. + assert "for (const auto &method : AllMethodSchemas())" in source + assert "entry.request_schema = method.params;" in source + assert "entry.schema = method.result;" in source + assert "m.emplace(method.method, std::move(entry));" in source + records = (path.parent / "generated" / "vgi_protocol_records.cpp").read_text() + method_table = records.split("&AllMethodSchemas()", 1)[1].split("return methods;", 1)[0] + declared = {method.name for method in registry_methods()} + registered = set(re.findall(r'\{"([a-z_]+)",\s*\w+ParamsSchema\(\),', method_table)) + assert registered == declared, ( + f"Native method contracts missing: {declared - registered}; stale: {registered - declared}" + ) + + def test_generator_is_deterministic() -> None: """Running the generator twice produces byte-identical output.""" out1 = io.StringIO() diff --git a/tests/test_generated_go_types.py b/tests/test_generated_go_types.py index c016bf35..e2cef253 100644 --- a/tests/test_generated_go_types.py +++ b/tests/test_generated_go_types.py @@ -85,7 +85,7 @@ def test_attach_result_shape() -> None: assert fields["comment"].go_type == "*string" assert fields["catalog_version"].go_type == "int64" last = record.fields[-1] - assert (last.wire_name, last.name, last.go_type) == ("supports_catalog_contents", "SupportsCatalogContents", "bool") + assert (last.wire_name, last.name, last.go_type) == ("query_pushdown", "QueryPushdown", "*QueryCapabilities") @pytest.mark.parametrize( diff --git a/tests/test_generated_java_types.py b/tests/test_generated_java_types.py index 2b7ee1d8..7a5dac3a 100644 --- a/tests/test_generated_java_types.py +++ b/tests/test_generated_java_types.py @@ -103,15 +103,14 @@ def _derive(java_type: str, annotations: str) -> pa.DataType: return _OVERRIDES[override.group(1)] if java_type.startswith("List<"): element = java_type[5:-1] - if element in _INFERRED: - return pa.list_(pa.field("item", _INFERRED[element], nullable=True)) - # A nested generated record derives as an inline struct of its components. - nested = _derived_schema(java_types.render(element)) - return pa.list_(pa.field("item", pa.struct(list(nested)), nullable=True)) + return pa.list_(pa.field("item", _derive(element, ""), nullable=True)) if java_type.startswith("Map<"): key, value = (t.strip() for t in java_type[4:-1].split(",")) return pa.map_(_INFERRED[key], _INFERRED[value]) - return _INFERRED[java_type] + if java_type in _INFERRED: + return _INFERRED[java_type] + # A nested generated record derives as an inline struct of its components. + return pa.struct(list(_derived_schema(java_types.render(java_type)))) def _derived_schema(text: str) -> pa.Schema: @@ -146,3 +145,9 @@ def test_unsupported_arrow_type_is_refused() -> None: java_types._component(pa.field("t", pa.timestamp("us")), "X") with pytest.raises(GeneratorError): java_types._component(pa.field("l", pa.list_(pa.field("item", pa.binary(), nullable=False))), "X") + with pytest.raises(GeneratorError): + java_types._component( + pa.field("nested", pa.list_(pa.list_(pa.field("item", pa.string(), nullable=False)))), "X" + ) + with pytest.raises(GeneratorError): + java_types._component(pa.field("nested", pa.list_(pa.list_(pa.field("renamed", pa.string())))), "X") diff --git a/tests/test_generated_registry.py b/tests/test_generated_registry.py index 14591660..f3fca45e 100644 --- a/tests/test_generated_registry.py +++ b/tests/test_generated_registry.py @@ -1,6 +1,6 @@ # Copyright 2025, 2026 Query Farm LLC - https://query.farm -"""The generated ``vgi.v2`` registries reproduce the reference protocol hash. +"""The generated ``vgi.v3`` registries reproduce the reference protocol hash. One parametrization runs over every backend of the registry generator (:data:`vgi.codegen._registry_backend.REGISTRY_BACKENDS`). The meaningful @@ -65,10 +65,10 @@ def test_model_reproduces_the_live_hash() -> None: @pytest.mark.parametrize("backend", _BACKENDS, ids=_IDS) def test_registry_derives_the_live_hash(backend: RegistryBackend) -> None: - """The rendered registry, read back as the SDK's vgi-rpc port reads it, is the reference vgi.v2.""" + """The rendered registry, read back as the SDK's vgi-rpc port reads it, is the reference vgi.v3.""" derived = backend.derived_methods() assert sorted(m.name for m in derived) == sorted(m.name for m in registry_methods()), ( - f"{backend.language}: rendered method set differs from vgi.v2" + f"{backend.language}: rendered method set differs from vgi.v3" ) assert preimage_hash(protocol_name(), derived) == _live_hash() diff --git a/tests/test_generated_ts_client.py b/tests/test_generated_ts_client.py index 9e1063a7..f0d5a7b7 100644 --- a/tests/test_generated_ts_client.py +++ b/tests/test_generated_ts_client.py @@ -79,6 +79,17 @@ def test_nullable_list_elements_keep_array_precedence() -> None: assert "argument_names?: (string | null)[] | null;" in buf.getvalue() +def test_binary_records_match_the_nonrecursive_codec() -> None: + """Nested IPC stays bytes until a caller explicitly decodes its record.""" + buf = io.StringIO() + emit(buf) + generated = buf.getvalue() + assert "definition: /** @ipc-record IndexDefinition */ Uint8Array;" in generated + assert "options: /** @ipc-record OptionState */ Uint8Array;" in generated + assert "expression?: /** @ipc-record SqlExpression */ Uint8Array | null;" in generated + assert "export interface IndexDefinition {" in generated + + def test_checked_in_client_matches_generator() -> None: """Checked-in vgi-client.ts matches what the generator would emit right now.""" path = _vgi_ts_client_path() diff --git a/tests/test_generated_ts_types.py b/tests/test_generated_ts_types.py index 580c3b6c..ff0b1cde 100644 --- a/tests/test_generated_ts_types.py +++ b/tests/test_generated_ts_types.py @@ -71,7 +71,7 @@ def test_builder_fields_follow_arrow_schema_order() -> None: names = [line.strip().split(":", 1)[0] for line in body.group(1).splitlines()] assert names == list(cls.ARROW_SCHEMA.names), name # type: ignore[attr-defined] assert " supports_catalog_contents: init.supports_catalog_contents ?? false," in text - assert ' default_schema: init.default_schema ?? "main",' in text + assert ' default_schema: init.default_schema === undefined ? "main" : init.default_schema,' in text def test_client_records_are_reexported() -> None: diff --git a/tests/test_grant_auth.py b/tests/test_grant_auth.py index 1d879037..36d43d10 100644 --- a/tests/test_grant_auth.py +++ b/tests/test_grant_auth.py @@ -7,7 +7,7 @@ vgi-python's own HTTP surface (``create_app``), not vgi-rpc in isolation: - with ``VGI_RPC_GRANT_KEYS`` set, ``issue_grant`` mints a sealed grant and a - ``vgi.v2`` call presenting it is authenticated as the grant's owner; + ``vgi.v3`` call presenting it is authenticated as the grant's owner; - a worker that overrides ``resolve_token`` has it consulted for bearers; - the shared vectors (``vgi_rpc/conformance/grant_token_vectors.json``) are accepted and rejected through the full chain; @@ -73,7 +73,7 @@ def _header_authenticate(req: Any) -> AuthContext: def _whoami(app: Any, *, bearer: str) -> str: - """Call ``peer_auth_echo`` (a vgi.v2 scalar function) and return its principal.""" + """Call ``peer_auth_echo`` (a vgi.v3 scalar function) and return its principal.""" client = _SyncTestClient(app, default_headers={"Authorization": f"Bearer {bearer}"}) with Client.from_http("http://vgi.test", httpx_client=client) as vgi: out = list( @@ -136,7 +136,7 @@ def test_keys_are_http_only(self, monkeypatch: pytest.MonkeyPatch) -> None: class TestSealedGrantEndToEnd: - """issue_grant with fresh auth, then a vgi.v2 call with ``Bearer ``, as the owner.""" + """issue_grant with fresh auth, then a vgi.v3 call with ``Bearer ``, as the owner.""" def test_minted_grant_authenticates_as_its_owner(self, monkeypatch: pytest.MonkeyPatch) -> None: """Minted grant authenticates as its owner.""" diff --git a/tests/test_hosted_protocols.py b/tests/test_hosted_protocols.py index 5d45e3d5..ec97287a 100644 --- a/tests/test_hosted_protocols.py +++ b/tests/test_hosted_protocols.py @@ -37,7 +37,7 @@ _REPO_ROOT = Path(__file__).resolve().parent.parent _EXTRAS = ["vgi_test.Echo.v1", "vgi_test.Counter.v1"] -_EXPECTED = ["vgi.v2", *_EXTRAS, "vgi_rpc.Reflection.v1"] +_EXPECTED = ["vgi.v3", *_EXTRAS, "vgi_rpc.Reflection.v1"] _IDENTITY = "vgi_rpc.Identity.v2" # Protocol classes are abstract to mypy; the connect helpers want concrete types. @@ -177,6 +177,9 @@ def test_http(self) -> None: assert _names(proxy) == _EXPECTED with http_connect(_ECHO, client=client) as echo: assert echo.echo(text="web") == "echo:web" + # An old major must not fall through to the incompatible v3 handler. + response = client.post("/vgi.v2/bind", content=b"", headers={}) + assert response.status_code == 404 class TestIdentityPlacement: @@ -219,12 +222,12 @@ class TestHookContract: """Called once; default empty; bad answers fail early and name the hook.""" def test_default_is_empty(self) -> None: - """A worker that does not override the hook hosts only ``vgi.v2`` and reflection.""" + """A worker that does not override the hook hosts only ``vgi.v3`` and reflection.""" from tests.test_serve import _SingleWorker assert tuple(_SingleWorker.hosted_protocols()) == () server = build_rpc_server(_SingleWorker(quiet=True), transport="pipe") - assert list(server.bindings) == ["vgi.v2", "vgi_rpc.Reflection.v1"] + assert list(server.bindings) == ["vgi.v3", "vgi_rpc.Reflection.v1"] def test_called_once_per_build(self) -> None: """The hook is consulted once per server, so its answer is fixed per process.""" @@ -242,7 +245,7 @@ def hosted_protocols(cls) -> Sequence[tuple[type, object]]: def test_describe_false_drops_only_reflection(self) -> None: """``--no-describe`` removes reflection, never the worker's extras.""" server = build_rpc_server(HostingWorker(quiet=True), transport="http", describe=False) - assert list(server.bindings) == ["vgi.v2", *_EXTRAS] + assert list(server.bindings) == ["vgi.v3", *_EXTRAS] def test_unknown_transport_rejected(self) -> None: """A typo in a transport label fails loudly rather than defaulting.""" @@ -279,10 +282,10 @@ def test_duplicate_name_rejected(self) -> None: build_rpc_server(worker, transport="pipe") def test_worker_own_protocol_name_rejected(self) -> None: - """An extra may not shadow ``vgi.v2``.""" + """An extra may not shadow ``vgi.v3``.""" class _Shadow(Protocol): - protocol_name: ClassVar[str] = "vgi.v2" + protocol_name: ClassVar[str] = "vgi.v3" def echo(self, text: str) -> str: ... @@ -376,11 +379,11 @@ class TestFixtureWorker: """The fixture worker hosts the vgi-rpc reference secondary, and opts into Identity on request.""" def test_example_worker_hosts_the_secondary(self) -> None: - """``vgi-fixture-worker`` lists ``vgi.v2`` then ``conformance.Secondary.v1``.""" + """``vgi-fixture-worker`` lists ``vgi.v3`` then ``conformance.Secondary.v1``.""" from vgi._test_fixtures.worker import ExampleWorker server = build_rpc_server(ExampleWorker(quiet=True), transport="pipe") - assert list(server.bindings)[:2] == ["vgi.v2", "conformance.Secondary.v1"] + assert list(server.bindings)[:2] == ["vgi.v3", "conformance.Secondary.v1"] def test_identity_fixture_hosts_identity_with_the_conformance_allowlist(self) -> None: """``vgi-fixture-http --identity`` builds Identity without reading the environment.""" diff --git a/tests/test_http_demo_storage.py b/tests/test_http_demo_storage.py index a5d5ee36..7c61feb8 100644 --- a/tests/test_http_demo_storage.py +++ b/tests/test_http_demo_storage.py @@ -5,12 +5,14 @@ from __future__ import annotations import os -import socket import subprocess import sys import time from collections.abc import Iterator from contextlib import contextmanager +from pathlib import Path +from tempfile import TemporaryDirectory +from urllib.parse import urlsplit import httpx2 import pyarrow as pa @@ -26,20 +28,13 @@ # --------------------------------------------------------------------------- -def _free_port() -> int: - """Allocate an available localhost TCP port.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("127.0.0.1", 0)) - return int(s.getsockname()[1]) - - @contextmanager def _run_demo_http_server( *, - port: int, + port: int = 0, threshold_bytes: int, compression: str = "none", -) -> Iterator[None]: +) -> Iterator[str]: """Run vgi._test_fixtures.http_server with --demo-storage in a subprocess.""" cmd = [ sys.executable, @@ -58,43 +53,52 @@ def _run_demo_http_server( compression, ] env = os.environ.copy() - proc = subprocess.Popen( - cmd, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - try: - yield - finally: - proc.terminate() + with TemporaryDirectory(prefix="vgi-demo-http-") as directory: + port_file = Path(directory) / "port" + proc = subprocess.Popen( + [*cmd, "--port-file", str(port_file)], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) try: - proc.wait(timeout=10) - except subprocess.TimeoutExpired: - proc.kill() - proc.wait(timeout=5) - - # POSIX terminate() -> SIGTERM (-15); Windows terminate() is - # TerminateProcess(handle, 1), so a cleanly-stopped worker reports 1. - ok_codes = (0, 1) if sys.platform == "win32" else (0, -15) - if proc.returncode not in ok_codes: - stderr = proc.stderr.read() if proc.stderr is not None else "" - raise RuntimeError(f"example HTTP worker exited with code {proc.returncode}: {stderr}") + yield _wait_for_http_server(proc, port_file) + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=5) + + # POSIX terminate() -> SIGTERM (-15); Windows terminate() is + # TerminateProcess(handle, 1), so a cleanly-stopped worker reports 1. + ok_codes = (0, 1) if sys.platform == "win32" else (0, -15) + if proc.returncode not in ok_codes: + stderr = proc.stderr.read() if proc.stderr is not None else "" + raise RuntimeError(f"example HTTP worker exited with code {proc.returncode}: {stderr}") -def _wait_for_http_server(base_url: str) -> None: - """Wait until the HTTP server responds to capabilities requests.""" +def _wait_for_http_server(proc: subprocess.Popen[str], port_file: Path) -> str: + """Wait for this live child to bind and answer capabilities requests.""" from vgi_rpc.http import http_capabilities - deadline = time.time() + 30 - while time.time() < deadline: + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + if proc.poll() is not None: + stderr = proc.stderr.read() if proc.stderr is not None else "" + raise RuntimeError(f"example HTTP worker exited with code {proc.returncode}: {stderr}") + if not port_file.exists(): + time.sleep(0.05) + continue + base_url = f"http://127.0.0.1:{int(port_file.read_text())}" try: http_capabilities(base_url=base_url) - return + return base_url except Exception: time.sleep(0.25) - raise TimeoutError(f"Timed out waiting for HTTP server at {base_url}") + raise TimeoutError(f"Timed out waiting for HTTP worker {proc.pid} to bind and serve") def _batch_to_ipc_bytes(batch: pa.RecordBatch) -> bytes: @@ -239,16 +243,10 @@ def test_demo_output_offload_large_response(compression: str, expected_extension payload_bytes = threshold_bytes + 1024 seed = 12345 - port = _free_port() - base_url = f"http://127.0.0.1:{port}" - with _run_demo_http_server( - port=port, threshold_bytes=threshold_bytes, compression=compression, - ): - _wait_for_http_server(base_url) - + ) as base_url: caps = http_capabilities(base_url=base_url) assert caps.upload_url_support assert caps.max_upload_bytes == threshold_bytes @@ -323,12 +321,7 @@ def test_demo_input_upload_url_then_exchange(compression: str) -> None: # threshold, so a value below the state-token size would 413 every # exchange before any user payload is even considered. threshold_bytes = 16 * 1024 - port = _free_port() - base_url = f"http://127.0.0.1:{port}" - - with _run_demo_http_server(port=port, threshold_bytes=threshold_bytes): - _wait_for_http_server(base_url) - + with _run_demo_http_server(threshold_bytes=threshold_bytes) as base_url: caps = http_capabilities(base_url=base_url) assert caps.upload_url_support assert caps.max_upload_bytes == threshold_bytes @@ -392,12 +385,7 @@ def test_demo_capabilities_advertised() -> None: from vgi_rpc.http import http_capabilities threshold_bytes = 8192 - port = _free_port() - base_url = f"http://127.0.0.1:{port}" - - with _run_demo_http_server(port=port, threshold_bytes=threshold_bytes): - _wait_for_http_server(base_url) - + with _run_demo_http_server(threshold_bytes=threshold_bytes) as base_url: caps = http_capabilities(base_url=base_url) assert caps.upload_url_support is True assert caps.max_upload_bytes == threshold_bytes @@ -408,11 +396,31 @@ def test_demo_blob_404() -> None: """GET for a nonexistent blob should return 404.""" pytest.importorskip("vgi_rpc.http") - port = _free_port() - base_url = f"http://127.0.0.1:{port}" - - with _run_demo_http_server(port=port, threshold_bytes=4096): - _wait_for_http_server(base_url) - + with _run_demo_http_server(threshold_bytes=4096) as base_url: resp = httpx2.get(f"{base_url}/__blobs__/nonexistent.arrow", timeout=10.0) assert resp.status_code == 404 + + +def test_demo_servers_reserve_distinct_bound_ports() -> None: + """Concurrent demo workers retain separate listener and capability identities.""" + from vgi_rpc.http import http_capabilities + + with ( + _run_demo_http_server(threshold_bytes=8192) as first, + _run_demo_http_server(threshold_bytes=16384) as second, + ): + assert first != second + assert http_capabilities(base_url=first).max_upload_bytes == 8192 + assert http_capabilities(base_url=second).max_upload_bytes == 16384 + + +def test_demo_startup_rejects_an_occupied_port() -> None: + """An existing server cannot make a child that failed to bind look ready.""" + with _run_demo_http_server(threshold_bytes=8192) as first: + port = urlsplit(first).port + assert port is not None + with ( + pytest.raises(RuntimeError, match="HTTP worker exited"), + _run_demo_http_server(port=port, threshold_bytes=16384), + ): + pytest.fail("a child that did not bind must never become ready") diff --git a/tests/test_logical_types.py b/tests/test_logical_types.py new file mode 100644 index 00000000..f8bac38e --- /dev/null +++ b/tests/test_logical_types.py @@ -0,0 +1,293 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Canonical Arrow Variant transport and distinct tuple identity.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import cast + +import pyarrow as pa +import pytest + +from vgi.logical_types import ( + EXTENSION_METADATA_KEY, + EXTENSION_NAME_KEY, + LOGICAL_TYPE_KEY, + LOGICAL_TYPE_PROFILES, + MAX_VARIANT_BYTES, + TUPLE_PROFILE, + VARIANT_EXTENSION_NAME, + VARIANT_PROFILE, + require_logical_batch, + require_logical_profiles, + require_logical_values, + select_logical_type_profiles, + tuple_field, + validate_logical_schema, + validate_logical_values, + variant_field, +) +from vgi.typed_values import TypedValueBatch + +# Apache parquet-format VariantEncoding.md: metadata version1, empty dictionary; +# encoded null; empty object; and a one-key object {"present": null}. +# https://github.com/apache/parquet-format/blob/master/VariantEncoding.md +EMPTY_METADATA = bytes.fromhex("010000") +NULL_VALUE = bytes.fromhex("00") +EMPTY_OBJECT = bytes.fromhex("020000") +PRESENT_METADATA = bytes.fromhex("0101000770726573656e74") +PRESENT_NULL = bytes.fromhex("020100000100") + + +def values( + field: pa.Field[pa.DataType], value: object, *, metadata: dict[bytes | str, bytes | str] | None = None +) -> TypedValueBatch: + """Construct an explicit one-row Arrow carrier, with no variant encoder.""" + return TypedValueBatch( + pa.RecordBatch.from_arrays([pa.array([value], type=field.type)], schema=pa.schema([field], metadata=metadata)) + ) + + +def test_variant_field_uses_the_canonical_arrow_extension() -> None: + field = variant_field("v") + assert field.metadata == {EXTENSION_NAME_KEY: VARIANT_EXTENSION_NAME, EXTENSION_METADATA_KEY: b""} + assert field.type == pa.struct([pa.field("metadata", pa.binary(), nullable=False), pa.field("value", pa.binary())]) + assert VARIANT_PROFILE == "vgi.type.variant.parquet.v1" + with pytest.raises(ValueError, match="not negotiated"): + require_logical_profiles(pa.schema([field])) + + +@pytest.mark.parametrize( + "metadata,payload", [(EMPTY_METADATA, NULL_VALUE), (EMPTY_METADATA, EMPTY_OBJECT), (PRESENT_METADATA, PRESENT_NULL)] +) +def test_standard_variant_bytes_and_metadata_survive_arrow_ipc(metadata: bytes, payload: bytes) -> None: + original = values(variant_field("v", metadata={b"owner": b"preserved"}), {"metadata": metadata, "value": payload}) + restored = TypedValueBatch.deserialize_from_bytes(original.serialize_to_bytes()) + validate_logical_values(restored, selected_profiles=[VARIANT_PROFILE]) + assert restored.schema.equals(original.schema, check_metadata=True) + assert restored.batch.column(0)[0].as_py() == {"metadata": metadata, "value": payload} + + +def test_outer_sql_null_present_variant_null_and_absent_member_remain_distinct() -> None: + field = variant_field("v") + batch = pa.RecordBatch.from_arrays( + [ + pa.array( + [ + None, + {"metadata": EMPTY_METADATA, "value": NULL_VALUE}, + {"metadata": EMPTY_METADATA, "value": EMPTY_OBJECT}, + {"metadata": PRESENT_METADATA, "value": PRESENT_NULL}, + ], + type=field.type, + ) + ], + schema=pa.schema([field]), + ) + require_logical_batch(batch, selected_profiles=[VARIANT_PROFILE]) + assert not batch.column(0)[0].is_valid + assert batch.column(0)[1].is_valid + assert batch.column(0)[1].as_py()["value"] == NULL_VALUE + assert batch.column(0)[2].as_py()["value"] != batch.column(0)[3].as_py()["value"] + + +@pytest.mark.parametrize("binary_type", [pa.binary(), pa.large_binary(), pa.binary_view()]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_canonical_binary_layouts_and_field_order(binary_type: pa.DataType, reverse: bool) -> None: + fields = [pa.field("metadata", binary_type, nullable=False), pa.field("value", binary_type)] + if reverse: + fields.reverse() + field = pa.field("v", pa.struct(fields), metadata=variant_field("v").metadata) + original = values(field, {"metadata": EMPTY_METADATA, "value": NULL_VALUE}) + validate_logical_values(original, selected_profiles=[VARIANT_PROFILE]) + assert original.batch.column(0)[0].as_py()["metadata"] == EMPTY_METADATA + + +@pytest.mark.parametrize("encoded", ["dictionary", "run_end"]) +def test_canonical_metadata_encoding_wrappers(encoded: str) -> None: + metadata: pa.Array[pa.Scalar[pa.DataType]] + if encoded == "dictionary": + metadata = pa.DictionaryArray.from_arrays(pa.array([0], type=pa.int8()), pa.array([EMPTY_METADATA])) + else: + metadata = pa.RunEndEncodedArray.from_arrays(pa.array([1], type=pa.int16()), pa.array([EMPTY_METADATA])) + field = pa.field( + "v", + pa.struct([pa.field("metadata", metadata.type, nullable=False), pa.field("value", pa.binary())]), + metadata=variant_field("v").metadata, + ) + array = pa.StructArray.from_arrays([metadata, pa.array([NULL_VALUE])], fields=list(field.type)) + require_logical_batch( + pa.RecordBatch.from_arrays([array], schema=pa.schema([field])), selected_profiles=[VARIANT_PROFILE] + ) + + +class CanonicalVariantType(pa.ExtensionType): + """Represent an already registered canonical type without a value decoder.""" + + def __init__(self) -> None: + """Declare the canonical storage without a binary value decoder.""" + super().__init__(variant_field("v").type, "arrow.parquet.variant") + + def __arrow_ext_serialize__(self) -> bytes: + """Return the canonical empty extension metadata.""" + return b"" + + @classmethod + def __arrow_ext_deserialize__(cls, storage_type: pa.DataType, serialized: bytes) -> CanonicalVariantType: + """Restore the canonical schema, preserving the standard marker.""" + assert serialized == b"" + assert storage_type == variant_field("v").type + return cls() + + +def test_registered_extension_and_dictionary_wrapping_do_not_bypass_negotiation() -> None: + dtype = CanonicalVariantType() + storage = pa.array([{"metadata": EMPTY_METADATA, "value": NULL_VALUE}], type=dtype.storage_type) + extension = pa.ExtensionArray.from_storage(dtype, storage) + for array in (extension, pa.DictionaryArray.from_arrays(pa.array([0], type=pa.int8()), extension)): + batch = pa.RecordBatch.from_arrays([array], names=["v"]) + with pytest.raises(ValueError, match="not negotiated"): + require_logical_batch(batch) + require_logical_batch(batch, selected_profiles=[VARIANT_PROFILE]) + + +def test_variant_nested_in_tuple_requires_both_profiles() -> None: + field = tuple_field("pair", [variant_field("ignored")]) + original = values(field, {"element1": {"metadata": EMPTY_METADATA, "value": NULL_VALUE}}) + with pytest.raises(ValueError, match="variant.*not negotiated"): + require_logical_values(original, selected_profiles=[TUPLE_PROFILE]) + validate_logical_values(original, selected_profiles=LOGICAL_TYPE_PROFILES) + + +def test_variant_payload_semantics_remain_the_backends_responsibility() -> None: + # This helper deliberately does not pretend to implement a Parquet decoder. + opaque = values(variant_field("v"), {"metadata": b"not-decoded-here", "value": b"opaque-bytes"}) + validate_logical_values(opaque, selected_profiles=[VARIANT_PROFILE]) + + +def test_missing_required_metadata_fails_before_output() -> None: + field = variant_field("v") + batch = pa.RecordBatch.from_arrays( + [ + pa.array( + [{"metadata": EMPTY_METADATA, "value": NULL_VALUE}, {"metadata": None, "value": NULL_VALUE}], + type=field.type, + ) + ], + schema=pa.schema([field]), + ) + with pytest.raises(ValueError, match="non-NULL metadata"): + require_logical_batch(batch, selected_profiles=[VARIANT_PROFILE]) + + +@pytest.mark.parametrize("row", [{"metadata": EMPTY_METADATA, "value": None}, {"metadata": b"", "value": b""}]) +def test_carrier_preserves_nullable_value_and_opaque_empty_bytes(row: dict[str, bytes | None]) -> None: + original = values(variant_field("v"), row) + require_logical_values(original, selected_profiles=[VARIANT_PROFILE]) + assert original.batch.column(0)[0].as_py() == row + + +def test_present_variant_byte_budget_applies_to_both_binary_fields() -> None: + field = variant_field("v") + large = values(field, {"metadata": EMPTY_METADATA, "value": b"x" * MAX_VARIANT_BYTES}) + with pytest.raises(ValueError, match="byte budget"): + require_logical_values(large, selected_profiles=[VARIANT_PROFILE]) + + +@pytest.mark.parametrize( + "storage", + [ + pa.binary(), + pa.struct([pa.field("metadata", pa.binary()), pa.field("value", pa.binary())]), + pa.struct([pa.field("metadata", pa.binary(), nullable=False)]), + pa.struct([pa.field("metadata", pa.binary(), nullable=False), pa.field("value", pa.string())]), + pa.struct( + [ + pa.field("metadata", pa.binary(), nullable=False), + pa.field("value", pa.binary()), + pa.field("typed_value", pa.int64()), + ] + ), + pa.struct( + [ + pa.field("metadata", pa.binary(), nullable=False), + pa.field("metadata", pa.binary()), + pa.field("value", pa.binary()), + ] + ), + ], +) +def test_invalid_or_shredded_canonical_storage_is_rejected(storage: pa.DataType) -> None: + field = pa.field("v", storage, metadata=variant_field("v").metadata) + with pytest.raises(ValueError): + validate_logical_schema(pa.schema([field]), selected_profiles=[VARIANT_PROFILE]) + + +@pytest.mark.parametrize( + "kind", [b"variant", b"hugeint", b"uhugeint", b"bignum", b"bit", b"uuid", b"time_tz", b"interval", b"geometry"] +) +def test_removed_vgi_scalar_and_variant_annotations_are_not_accepted(kind: bytes) -> None: + field = pa.field("v", pa.binary(), metadata={LOGICAL_TYPE_KEY: kind}) + with pytest.raises(ValueError, match="unsupported logical type annotation"): + require_logical_profiles(pa.schema([field]), selected_profiles=LOGICAL_TYPE_PROFILES) + + +def test_nonempty_extension_metadata_and_duplicate_tags_are_rejected() -> None: + field = variant_field("v") + # PyArrow accepts duplicate-preserving metadata pairs; its stubs only declare dictionaries. + with_metadata = cast( + "Callable[[dict[bytes, bytes] | list[tuple[bytes, bytes]]], pa.Field[pa.DataType]]", field.with_metadata + ) + for metadata in ( + {EXTENSION_NAME_KEY: VARIANT_EXTENSION_NAME, EXTENSION_METADATA_KEY: b"{}"}, + [(EXTENSION_NAME_KEY, VARIANT_EXTENSION_NAME), (EXTENSION_NAME_KEY, VARIANT_EXTENSION_NAME)], + ): + with pytest.raises(ValueError): + validate_logical_schema(pa.schema([with_metadata(metadata)]), selected_profiles=[VARIANT_PROFILE]) + + +def test_tuple_identity_is_distinct_from_struct_and_survives_nested_ipc() -> None: + child = tuple_field("pair", [pa.field("", pa.int32()), pa.field("", pa.string())]) + field = pa.field("outer", pa.list_(child)) + original = values(field, [{"element1": 12, "element2": None}]) + restored = TypedValueBatch.deserialize_from_bytes(original.serialize_to_bytes()) + validate_logical_values(restored, selected_profiles=[TUPLE_PROFILE]) + assert restored.schema.equals(original.schema, check_metadata=True) + assert not child.equals(child.remove_metadata(), check_metadata=True) + with pytest.raises(ValueError, match="not negotiated"): + validate_logical_schema(restored.schema) + + +def test_empty_tuple_and_legacy_types_remain_distinct() -> None: + original = values(tuple_field("value", []), {}) + validate_logical_values(original, selected_profiles=[TUPLE_PROFILE]) + metadata = original.schema.field(0).metadata + assert metadata is not None + assert metadata[LOGICAL_TYPE_KEY] == b"tuple" + legacy = pa.schema([pa.field("legacy", pa.map_(pa.string(), pa.int32()))]) + assert not require_logical_profiles(legacy) + # Arrow accepts a Field here to retain child metadata; the stub only accepts DataType. + map_with_field = cast("Callable[[pa.DataType, pa.Field[pa.DataType]], pa.DataType]", pa.map_) + annotated = pa.schema([pa.field("legacy", map_with_field(pa.string(), tuple_field("value", [])))]) + with pytest.raises(ValueError, match="not negotiated"): + require_logical_profiles(annotated) + require_logical_profiles(annotated, selected_profiles=[TUPLE_PROFILE]) + + +def test_profile_selection_excludes_the_removed_custom_encoding() -> None: + assert select_logical_type_profiles([], LOGICAL_TYPE_PROFILES) == [] + assert select_logical_type_profiles( + ["vgi.type.variant.arrow.v1", VARIANT_PROFILE, TUPLE_PROFILE], LOGICAL_TYPE_PROFILES + ) == sorted(LOGICAL_TYPE_PROFILES) + + +def test_output_carrier_respects_nonnullable_value_declaration() -> None: + field = variant_field("v") + field = field.with_type(pa.struct([field.type.field("metadata"), field.type.field("value").with_nullable(False)])) + batch = pa.RecordBatch.from_arrays( + [pa.array([{"metadata": EMPTY_METADATA, "value": None}], type=field.type)], schema=pa.schema([field]) + ) + with pytest.raises(ValueError, match="non-NULL value"): + require_logical_batch(batch, selected_profiles=[VARIANT_PROFILE]) diff --git a/tests/test_projection_expressions.py b/tests/test_projection_expressions.py new file mode 100644 index 00000000..f63111f0 --- /dev/null +++ b/tests/test_projection_expressions.py @@ -0,0 +1,344 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Pure scalar negotiation validates contracts before existing stream execution.""" + +from __future__ import annotations + +import json +from contextlib import closing +from dataclasses import replace +from pathlib import Path +from typing import TypedDict, cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient +from vgi_rpc.rpc import AuthContext, CallContext, RpcError, StreamSession, rpc_methods +from vgi_rpc.wire import write_request + +from vgi.arguments import Arguments +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import SqlExpression +from vgi.client import Client +from vgi.invocation import BindResponse, FunctionType +from vgi.meta_worker import MetaWorker +from vgi.protocol import ( + BindRequest, + CatalogAttachRequest, + InitRequest, + TableFunctionProjectionRequest, + TableFunctionProjectionResponse, + VgiProtocol, +) +from vgi.serve import create_app +from vgi.table_function import BindParams, TableFunctionBase + +from .projection_reference import BASE, ROWS, SOURCE, ProjectionReferenceWorker + + +def expression(sql: str, *, owner: list[str] | None = None, context: QueryEvaluationContext | None = None) -> bytes: + return SqlExpression( + sql=sql, + profile=ROW_SCALAR_EXPRESSION, + schema_path=owner or ["main"], + evaluation_context=context or QueryEvaluationContext(), + ).serialize_to_bytes() + + +def request( + *sql: str, function: str = "projection_accepted", token: bytes | None = None, base: BindResponse | None = None +) -> TableFunctionProjectionRequest: + return TableFunctionProjectionRequest( + bind_call=BindRequest( + function_name=function, + function_type=FunctionType.TABLE, + schema_path=["main"], + arguments=Arguments(), + attach_opaque_data=token, + ), + bind_result=base + or BindResponse(output_schema=SOURCE, opaque_data=BASE, projection_expression_profiles=[ROW_SCALAR_EXPRESSION]), + expressions=[expression(value) for value in sql], + result_schema=pa.schema( + [pa.field(f"p{i}", pa.int64(), metadata={b"slot": str(i).encode()}) for i in range(len(sql))] + ), + ) + + +def accepted( + proposal: TableFunctionProjectionRequest, indices: list[int] | None = None +) -> TableFunctionProjectionResponse: + indices = [0] if indices is None else indices + source = proposal.bind_result.output_schema + metadata: dict[bytes | str, bytes | str] | None = ( + None if source.metadata is None else {key: value for key, value in source.metadata.items()} + ) + return TableFunctionProjectionResponse( + accepted_indices=indices, + bind_result=BindResponse( + output_schema=pa.schema([*source, *(proposal.result_schema.field(i) for i in indices)], metadata=metadata), + opaque_data=b"complete replacement", + projection_expression_profiles=proposal.bind_result.projection_expression_profiles, + repeatability=proposal.bind_result.repeatability, + ), + ) + + +def test_request_response_canonical_roundtrip_and_partial_append_contract() -> None: + proposal = request('CAST("n" AS BIGINT)', "n + 1", 'CAST("other" AS BIGINT)') + restored = TableFunctionProjectionRequest.deserialize_from_bytes(proposal.serialize_to_bytes()) + assert restored.bind_result.output_schema.equals(SOURCE, check_metadata=True) + response = accepted(restored, [0, 2]) + response.validate_request(restored) + decoded = TableFunctionProjectionResponse.deserialize_from_bytes(response.serialize_to_bytes()) + decoded.validate_request(restored) + assert decoded.accepted_indices == [0, 2] + assert decoded.bind_result is not None + assert decoded.bind_result.output_schema.names == ["n", "other", "text", "p0", "p2"] + assert decoded.bind_result.opaque_data == b"complete replacement" + TableFunctionProjectionResponse().validate_request(restored) + + +@pytest.mark.parametrize("indices", [[0, 0], [1, 0], [-1], [True]]) +def test_invalid_accepted_indices_reject(indices: list[int]) -> None: + with pytest.raises(ValueError, match="indices"): + TableFunctionProjectionResponse(accepted_indices=indices) + + +class _ProposalChange(TypedDict, total=False): + """The fields varied by malformed proposal fixtures.""" + + expressions: list[bytes] + result_schema: pa.Schema + bind_result: BindResponse + + +@pytest.mark.parametrize( + "change", + [ + {"expressions": []}, + {"expressions": [b"invalid"]}, + {"expressions": [expression("n", owner=["a.b"])]}, + {"result_schema": pa.schema([pa.field("n", pa.int64())])}, + {"result_schema": pa.schema([pa.field("p0", pa.int64())], metadata={b"wrong": b"scope"})}, + {"bind_result": BindResponse(output_schema=SOURCE)}, + { + "bind_result": BindResponse( + output_schema=SOURCE, lookup_names=["orphan"], projection_expression_profiles=[ROW_SCALAR_EXPRESSION] + ) + }, + ], +) +def test_invalid_proposals_reject_before_callback(change: _ProposalChange) -> None: + with pytest.raises((ValueError, pa.ArrowInvalid)): + replace(request("n"), **change) + + +def test_changed_contexts_and_unoffered_profiles_reject() -> None: + proposal = request("n", "other") + with pytest.raises(ValueError, match="context"): + replace( + proposal, + expressions=[expression("n"), expression("other", context=QueryEvaluationContext(default_order="desc"))], + ) + with pytest.raises(ValueError, match="profile"): + replace( + proposal, + expressions=[ + replace(SqlExpression.deserialize_from_bytes(e), profile="other.expression.v1").serialize_to_bytes() + for e in proposal.expressions + ], + ) + + +def test_column_name_identity_uses_ascii_case_insensitive_comparison() -> None: + with pytest.raises(ValueError, match="unambiguous"): + replace(request("n"), result_schema=pa.schema([pa.field("N", pa.int64())])) + source = BindResponse( + output_schema=pa.schema([pa.field("ß", pa.int32())]), + projection_expression_profiles=[ROW_SCALAR_EXPRESSION], + ) + proposal = replace(request('"ß"', base=source), result_schema=pa.schema([pa.field("ss", pa.int64())])) + proposal.__post_init__() + + +@pytest.mark.parametrize("change", ["prefix", "type", "nullability", "metadata", "count", "offer", "secret", "index"]) +def test_malformed_acceptance_cannot_change_the_plan(change: str) -> None: + proposal = request("n") + response = accepted(proposal) + bound = response.bind_result + assert bound is not None + schema = bound.output_schema + if change == "prefix": + schema = schema.set(0, schema.field(0).with_name("renamed")) + elif change == "type": + schema = schema.set(3, schema.field(3).with_type(pa.string())) + elif change == "nullability": + schema = schema.set(3, schema.field(3).with_nullable(False)) + elif change == "metadata": + schema = schema.remove_metadata() + elif change == "count": + schema = SOURCE + with pytest.raises(ValueError): + if change == "secret": + replace(response, bind_result=replace(bound, lookup_names=["secret"])) + elif change == "index": + replace(response, accepted_indices=[1]).validate_request(proposal) + else: + replace( + response, + bind_result=replace( + bound, + output_schema=schema, + projection_expression_profiles=[] if change == "offer" else bound.projection_expression_profiles, + ), + ).validate_request(proposal) + assert proposal.bind_result.output_schema.equals(SOURCE, check_metadata=True) + + +def test_default_callback_declines_and_corrupted_envelopes_reject() -> None: + proposal = request("n") + # The default callback does not inspect bind parameters. Exercise it directly. + assert ( + TableFunctionBase[None].on_projection(cast("BindParams[None]", None), proposal) + == TableFunctionProjectionResponse() + ) + payload = proposal.serialize_to_bytes() + with pytest.raises(ValueError, match="trailing"): + TableFunctionProjectionRequest.deserialize_from_bytes(payload + b"garbage") + with pa.ipc.open_stream(payload) as reader: + row = reader.read_next_batch() + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, row.schema) as writer: + writer.write_batch(row) + writer.write_batch(row) + with pytest.raises(ValueError, match="exactly one"): + TableFunctionProjectionRequest.deserialize_from_bytes(sink.getvalue().to_pybytes()) + + +def client() -> Client: + return Client.from_http( + "http://vgi.test", + httpx_client=_SyncTestClient( + create_app(ProjectionReferenceWorker, describe=False, signing_key=b"projection-test-signing-key") + ), + ) + + +def test_http_truncated_projection_body_is_a_bad_request() -> None: + """A truncated custom-codec value must be rejected as malformed input.""" + transport = _SyncTestClient( + create_app(ProjectionReferenceWorker, describe=False, signing_key=b"projection-test-signing-key") + ) + payload = request('CAST("n" AS BIGINT)').serialize_to_bytes() + body = write_request( + "table_function_projection", + rpc_methods(VgiProtocol)["table_function_projection"].params_schema, + {"request": payload[:-9]}, + protocol=VgiProtocol.protocol_name, + protocol_version=VgiProtocol.protocol_version, + ) + response = transport.post( + f"http://vgi.test/{VgiProtocol.protocol_name}/table_function_projection", + content=body, + headers={"content-type": "application/vnd.apache.arrow.stream"}, + ) + assert response.status_code == 400 + + +def scan(proxy: VgiProtocol, call: BindRequest, bound: BindResponse) -> list[dict[str, object]]: + # RPC clients expose a StreamSession where the service declaration returns Stream. + execution = cast( + StreamSession, + proxy.init( + request=InitRequest(bind_call=call, output_schema=bound.output_schema, bind_opaque_data=bound.opaque_data) + ), + ) + with closing(execution) as stream: + batches = [output.batch for output in stream] + assert len(batches) == 2 # Separate HTTP turns rehydrate the complete accepted state. + assert all(batch.schema.equals(bound.output_schema, check_metadata=True) for batch in batches) + return pa.Table.from_batches(batches).to_pylist() + + +@pytest.mark.parametrize("mode,indices", [("accepted", [0, 2]), ("partial", [0]), ("declined", [])]) +def test_signed_http_partial_acceptance_decline_and_existing_stream_lifetime( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mode: str, indices: list[int] +) -> None: + trace = tmp_path / "events.jsonl" + monkeypatch.setenv("VGI_PROJECTION_TRACE", str(trace)) + with client() as c: + token = c.catalog_attach( + name="projection_reference", data_version_spec=None, implementation_version=None + ).attach_opaque_data + proposal = request( + 'CAST("n" AS BIGINT)', + "CAST(text AS BIGINT)", + 'CAST("other" AS BIGINT)', + function="projection_" + mode, + token=token, + ) + assert c._primary is not None + proxy = c._primary.proxy + original = proxy.bind(request=proposal.bind_call) + proposal = replace(proposal, bind_result=original) + result = proxy.table_function_projection(request=proposal) + result.validate_request(proposal) + assert result.accepted_indices == indices + assert all(json.loads(line)["event"] not in {"init", "rows"} for line in trace.read_text().splitlines()) + bound = result.bind_result or original + actual = scan(proxy, proposal.bind_call, bound) + expected = [dict(row, **{f"p{i}": row["n" if i == 0 else "other"] for i in indices}) for row in ROWS] + assert actual == expected + # The original bind is still valid: negotiation did not mutate cached source state. + assert scan(proxy, proposal.bind_call, original) == ROWS + + +def test_malformed_provider_response_rejects_before_init(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + trace = tmp_path / "events.jsonl" + monkeypatch.setenv("VGI_PROJECTION_TRACE", str(trace)) + with client() as c: + token = c.catalog_attach( + name="projection_reference", data_version_spec=None, implementation_version=None + ).attach_opaque_data + proposal = request('CAST("n" AS BIGINT)', function="projection_malformed", token=token) + assert c._primary is not None + with pytest.raises(RpcError, match="append-only"): + c._primary.proxy.table_function_projection(request=proposal) + assert all(json.loads(line)["event"] not in {"init", "rows"} for line in trace.read_text().splitlines()) + + +def test_accepted_execution_error_is_not_converted_to_decline_or_retried( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + trace = tmp_path / "events.jsonl" + monkeypatch.setenv("VGI_PROJECTION_TRACE", str(trace)) + with client() as c: + token = c.catalog_attach( + name="projection_reference", data_version_spec=None, implementation_version=None + ).attach_opaque_data + proposal = request('CAST("n" AS BIGINT)', function="projection_execution_error", token=token) + assert c._primary is not None + result = c._primary.proxy.table_function_projection(request=proposal) + assert result.accepted_indices == [0] + assert result.bind_result is not None + with pytest.raises(RpcError, match="failed after acceptance"): + scan(c._primary.proxy, proposal.bind_call, result.bind_result) + events = [json.loads(line)["event"] for line in trace.read_text().splitlines()] + assert events.count("proposal") == events.count("init") == 1 and "rows" not in events + + +def test_meta_worker_preserves_signed_owner_and_projection_contract() -> None: + worker = MetaWorker([ProjectionReferenceWorker(quiet=True)]) + worker._signing_key = b"projection-meta-signing-key" + ctx = CallContext(auth=AuthContext.anonymous(), emit_client_log=lambda *args, **kwargs: None) + token = worker.catalog_attach( + CatalogAttachRequest(name="projection_reference", data_version_spec=None, implementation_version=None) + ).attach_opaque_data + proposal = request('CAST("n" AS BIGINT)', token=token) + proposal = replace(proposal, bind_result=worker.bind(proposal.bind_call, ctx=ctx)) + response = worker.table_function_projection(proposal, ctx=ctx) + response.validate_request(proposal) + assert response.accepted_indices == [0] diff --git a/tests/test_regen_generated.py b/tests/test_regen_generated.py new file mode 100644 index 00000000..b77d8c15 --- /dev/null +++ b/tests/test_regen_generated.py @@ -0,0 +1,62 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Coordinated generation cannot silently omit an unavailable SDK checkout.""" + +import sys +from pathlib import Path + +import pytest + +from scripts import regen_generated + + +@pytest.fixture +def generated_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + """Use two SDK targets, one present and one missing, without real mutations.""" + monkeypatch.setattr( + regen_generated, + "_TARGETS", + [("present.module", "generated.txt", "present"), ("missing.module", "generated.txt", "missing")], + ) + monkeypatch.setattr(regen_generated, "_repo_root", lambda name, overrides: tmp_path if name == "present" else None) + monkeypatch.setattr(regen_generated, "_render", lambda name: f"{name}\n") + return tmp_path + + +@pytest.mark.parametrize("check", [False, True]) +def test_require_all_rejects_missing_checkout_before_writes( + generated_repo: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], check: bool +) -> None: + existing = generated_repo / "generated.txt" + existing.write_text("original\n") + argv = ["regen_generated.py", "--require-all"] + (["--check"] if check else []) + monkeypatch.setattr(sys, "argv", argv) + assert regen_generated.main() == 2 + assert existing.read_text() == "original\n" + assert "required repository missing is not checked out" in capsys.readouterr().err + + +def test_local_generation_still_allows_missing_checkouts( + generated_repo: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setattr(sys, "argv", ["regen_generated.py"]) + assert regen_generated.main() == 0 + assert (generated_repo / "generated.txt").read_text() == "present.module\n" + assert "skip missing (not checked out)" in capsys.readouterr().out + + +def test_only_requires_selected_repository(generated_repo: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, "argv", ["regen_generated.py", "--require-all", "--only", "present"]) + assert regen_generated.main() == 0 + assert (generated_repo / "generated.txt").read_text() == "present.module\n" + monkeypatch.setattr(sys, "argv", ["regen_generated.py", "--require-all", "--only", "missing"]) + assert regen_generated.main() == 2 + + +def test_require_all_check_rejects_missing_artifact(generated_repo: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, "argv", ["regen_generated.py", "--require-all", "--check", "--only", "present"]) + assert regen_generated.main() == 1 + assert not (generated_repo / "generated.txt").exists() + (generated_repo / "generated.txt").write_text("present.module\n") + assert regen_generated.main() == 0 diff --git a/tests/test_repeatability.py b/tests/test_repeatability.py new file mode 100644 index 00000000..070068b2 --- /dev/null +++ b/tests/test_repeatability.py @@ -0,0 +1,235 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""A bind-scoped promise must survive ordinary streams and source mutations.""" + +from __future__ import annotations + +from collections.abc import Iterator, Sequence +from contextlib import closing +from dataclasses import replace +from pathlib import Path +from typing import cast + +import pyarrow as pa +import pytest +from vgi_rpc.http._testing import _SyncTestClient +from vgi_rpc.rpc import RpcError, StreamSession + +from vgi.arguments import Arguments +from vgi.client import Client +from vgi.invocation import BindRepeatability, BindResponse, FunctionType +from vgi.protocol import BindRequest, InitRequest, VgiProtocol +from vgi.serve import create_app + +from .repeatability_reference import ( + BASELINE, + SOURCE, + BoundState, + RepeatabilityWorker, + Snapshot, + Stable, + replace_rows, + seed_database, +) +from .test_projection_expressions import accepted +from .test_projection_expressions import request as projection_request + +type Backend = tuple[Path, VgiProtocol, bytes] + +UPDATED = [(11, "eleven"), (12, "twelve"), (13, "thirteen"), (14, "fourteen")] + + +def expected(rows: Sequence[tuple[int | None, str]], generation: int) -> list[dict[str, int | str | None]]: + return [{"n": n, "label": label, "generation": generation} for n, label in rows] + + +def call(token: bytes | None, mode: str = "stable") -> BindRequest: + return BindRequest( + function_name="repeatability_" + mode, + function_type=FunctionType.TABLE, + schema_path=["main"], + arguments=Arguments(), + attach_opaque_data=token, + ) + + +def stream(proxy: VgiProtocol, invocation: BindRequest, bound: BindResponse) -> StreamSession: + # RPC clients expose a StreamSession where the service declaration returns Stream. + return cast( + StreamSession, + proxy.init( + request=InitRequest( + bind_call=invocation, output_schema=bound.output_schema, bind_opaque_data=bound.opaque_data + ) + ), + ) + + +def scan(proxy: VgiProtocol, invocation: BindRequest, bound: BindResponse) -> list[dict[str, object]]: + with closing(stream(proxy, invocation, bound)) as execution: + batches = [output.batch for output in execution] + assert len(batches) == 2 + assert all(batch.schema.equals(bound.output_schema, check_metadata=True) for batch in batches) + return pa.Table.from_batches(batches).to_pylist() + + +@pytest.fixture +def backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[Backend]: + database = tmp_path / "mutable.sqlite" + seed_database(database) + monkeypatch.setenv("VGI_REPEATABILITY_DB", str(database)) + app = create_app(RepeatabilityWorker, describe=False, signing_key=b"repeatability-http-test-key") + with Client.from_http("http://vgi.test", httpx_client=_SyncTestClient(app)) as client: + attached = client.catalog_attach( + name="repeatability_reference", data_version_spec=None, implementation_version=None + ) + assert client._primary is not None + assert attached.attach_opaque_data is not None + yield database, client._primary.proxy, attached.attach_opaque_data + + +def test_default_is_unknown_and_wire_uses_nonnullable_dictionary_enum() -> None: + value = BindResponse(output_schema=SOURCE) + assert value.repeatability is BindRepeatability.UNKNOWN + field = value.ARROW_SCHEMA.field("repeatability") + assert field.type == pa.dictionary(pa.int16(), pa.string()) and not field.nullable + for promise in BindRepeatability: + encoded = replace(value, repeatability=promise).serialize_to_bytes() + with pa.ipc.open_stream(encoded) as reader: + row = reader.read_next_batch() + assert row.column("repeatability").to_pylist() == [promise.name] + assert BindResponse.deserialize_from_bytes(encoded).repeatability is promise + + +@pytest.mark.parametrize("invalid", [None, "UNKNOWN", "stable_within_query", 1]) +def test_untyped_or_unknown_promises_reject(invalid: object) -> None: + with pytest.raises(ValueError, match="repeatability"): + BindResponse(output_schema=SOURCE, repeatability=cast(BindRepeatability, invalid)) + + +@pytest.mark.parametrize("wire_value", ["FUTURE_PROMISE", "unknown", "stable_within_query"]) +def test_unknown_wire_enum_rejects(wire_value: str) -> None: + value = BindResponse(output_schema=SOURCE) + with pa.ipc.open_stream(value.serialize_to_bytes()) as reader: + row = reader.read_next_batch() + index = row.schema.get_field_index("repeatability") + values = list(row.columns) + values[index] = pa.DictionaryArray.from_arrays(pa.array([0], type=pa.int16()), pa.array([wire_value])) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, row.schema) as writer: + writer.write_batch(pa.RecordBatch.from_arrays(values, schema=row.schema)) + with pytest.raises((KeyError, ValueError)): + BindResponse.deserialize_from_bytes(sink.getvalue().to_pybytes()) + + +def test_missing_and_semantically_null_dictionary_promise_reject() -> None: + with pa.ipc.open_stream(BindResponse(output_schema=SOURCE).serialize_to_bytes()) as reader: + row = reader.read_next_batch() + index = row.schema.get_field_index("repeatability") + with pytest.raises(ValueError, match="repeatability"): + BindResponse.deserialize_from_batch(row.remove_column(index)) + original = pa.array(["UNKNOWN"]) + # Retain the valid name bytes behind a null dictionary value. The index is valid. + null_value = pa.Array.from_buffers(pa.string(), 1, [pa.py_buffer(b"\x00"), *original.buffers()[1:]], null_count=1) + values = list(row.columns) + values[index] = pa.DictionaryArray.from_arrays(pa.array([0], type=pa.int16()), null_value) + malformed = pa.RecordBatch.from_arrays(values, schema=row.schema) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, row.schema) as writer: + writer.write_batch(malformed) + with pytest.raises(ValueError, match="repeatability"): + BindResponse.deserialize_from_bytes(sink.getvalue().to_pybytes()) + + +@pytest.mark.parametrize("promise", list(BindRepeatability)) +def test_projection_acceptance_preserves_promise_exactly(promise: BindRepeatability) -> None: + proposal = projection_request('CAST("n" AS BIGINT)') + proposal = replace(proposal, bind_result=replace(proposal.bind_result, repeatability=promise)) + response = accepted(proposal) + response.validate_request(proposal) + other = next(value for value in BindRepeatability if value is not promise) + assert response.bind_result is not None + changed = replace(response, bind_result=replace(response.bind_result, repeatability=other)) + with pytest.raises(ValueError, match="repeatability"): + changed.validate_request(proposal) + + +@pytest.mark.parametrize("mode", ["stable", "unknown"]) +def test_same_bind_snapshot_and_fresh_bind_after_external_commit(backend: Backend, mode: str) -> None: + database, proxy, token = backend + invocation = call(token, mode) + original = proxy.bind(request=invocation) + expected_promise = BindRepeatability.STABLE_WITHIN_QUERY if mode == "stable" else BindRepeatability.UNKNOWN + assert original.repeatability is expected_promise + assert scan(proxy, invocation, original) == expected(BASELINE, 1) + assert replace_rows(database, UPDATED) == 2 # Independently committed real source mutation. + assert scan(proxy, invocation, original) == (expected(BASELINE, 1) if mode == "stable" else expected(UPDATED, 2)) + fresh = proxy.bind(request=invocation) + assert scan(proxy, invocation, fresh) == expected(UPDATED, 2) + if mode == "stable": + assert fresh.opaque_data != original.opaque_data + # An independent bind does not mutate an earlier snapshot's portable state. + assert scan(proxy, invocation, original) == expected(BASELINE, 1) + + +@pytest.mark.parametrize("mode", ["stable", "unknown"]) +def test_http_continuation_does_not_reopen_mutable_source_between_batches(backend: Backend, mode: str) -> None: + database, proxy, token = backend + invocation = call(token, mode) + bound = proxy.bind(request=invocation) + with closing(stream(proxy, invocation, bound)) as execution: + outputs = iter(execution) + first = next(outputs).batch + replace_rows(database, UPDATED) + batches = [first, *(output.batch for output in outputs)] + assert len(batches) == 2 + assert pa.Table.from_batches(batches).to_pylist() == expected(BASELINE, 1) + + +def test_accepted_projection_retains_original_snapshot_after_external_commit(backend: Backend) -> None: + database, proxy, token = backend + invocation = call(token) + bound = proxy.bind(request=invocation) + proposal = replace(projection_request('CAST("n" AS BIGINT)', '"n" + 1'), bind_call=invocation, bind_result=bound) + response = proxy.table_function_projection(request=proposal) + assert response.accepted_indices == [0] + assert response.bind_result is not None + assert response.bind_result.repeatability is BindRepeatability.STABLE_WITHIN_QUERY + replace_rows(database, UPDATED) + assert scan(proxy, invocation, response.bind_result) == [dict(row, p0=row["n"]) for row in expected(BASELINE, 1)] + assert scan(proxy, invocation, bound) == expected(BASELINE, 1) + assert scan(proxy, invocation, proxy.bind(request=invocation)) == expected(UPDATED, 2) + + +def test_worker_revalidates_provider_repeatability_before_publication( + backend: Backend, monkeypatch: pytest.MonkeyPatch +) -> None: + _, proxy, token = backend + invalid = BindResponse(output_schema=SOURCE) + object.__setattr__(invalid, "repeatability", "STABLE_WITHIN_QUERY") + monkeypatch.setattr(Stable, "on_bind", classmethod(lambda cls, params: invalid)) + with pytest.raises(RpcError, match="repeatability"): + proxy.bind(request=call(token)) + + +def test_stale_inline_metadata_is_distinct_from_fresh_source_bind(backend: Backend) -> None: + _, proxy, token = backend + info = proxy.catalog_table_get( + attach_opaque_data=token, + schema_path=["main"], + name="stable_inline", + transaction_opaque_data=None, + at_unit=None, + at_value=None, + ).to_optional() + assert info is not None and info.bind_result is not None + bound = BindResponse.deserialize_from_bytes(info.bind_result) + assert bound.repeatability is BindRepeatability.STABLE_WITHIN_QUERY + assert bound.opaque_data is not None + state = BoundState.deserialize_from_bytes(bound.opaque_data) + assert state.snapshot is not None + stale = Snapshot.deserialize_from_bytes(state.snapshot) + assert stale.generation == 0 and stale.batch().column("n").to_pylist() == [-99] + invocation = call(token) + assert scan(proxy, invocation, proxy.bind(request=invocation)) == expected(BASELINE, 1) diff --git a/tests/test_typed_values.py b/tests/test_typed_values.py new file mode 100644 index 00000000..507bf8b7 --- /dev/null +++ b/tests/test_typed_values.py @@ -0,0 +1,203 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm +# ruff: noqa: D103 + +"""Lossless one-row Arrow carriers and rejection of ambiguous value payloads.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from decimal import Decimal +from typing import Annotated, cast + +import pyarrow as pa +import pytest +from vgi_rpc import ArrowSerializableDataclass, ArrowType + +from vgi.typed_values import TypedValueBatch + + +def _ipc(schema: pa.Schema, *batches: pa.RecordBatch) -> bytes: + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, schema) as writer: + for batch in batches: + writer.write_batch(batch) + return sink.getvalue().to_pybytes() + + +def test_typed_nulls_nested_metadata_and_values_round_trip() -> None: + """Retain distinctions a conversion through Python scalars would erase.""" + nested = pa.struct([pa.field("a.b", pa.int64(), nullable=False, metadata={b"child": b"annotation"})]) + schema = pa.schema( + [ + pa.field("record", nested, metadata={b"logical": b"provider.record"}), + pa.field("amount", pa.decimal128(20, 4)), + pa.field("time", pa.timestamp("ns", tz="America/New_York")), + pa.field("optional", pa.int32(), metadata={b"annotation": b"typed null"}), + ], + metadata={b"context": b"example"}, + ) + batch = pa.RecordBatch.from_pylist( + [{"record": {"a.b": 7}, "amount": Decimal("123.4500"), "time": 123456789, "optional": None}], + schema=schema, + ) + restored = TypedValueBatch.deserialize_from_bytes(TypedValueBatch(batch).serialize_to_bytes()) + assert restored.batch.equals(batch, check_metadata=True) + assert restored.schema.field("optional").type == pa.int32() + assert restored.batch.column("optional")[0].as_py() is None + + +def test_empty_values_are_one_row() -> None: + values = TypedValueBatch.empty(metadata={b"context": b"no parameters"}) + restored = TypedValueBatch.deserialize_from_bytes(values.serialize_to_bytes()) + assert restored.batch.num_rows == 1 + assert restored.batch.num_columns == 0 + assert restored.schema.metadata == values.schema.metadata + + +@pytest.mark.parametrize("ending", [b"", b"\0" * 4, b"\xff" * 4 + b"\0" * 4]) +def test_complete_arrow_stream_endings_remain_valid(ending: bytes) -> None: + values = TypedValueBatch(pa.record_batch({"x": [1]})) + payload = values.serialize_to_bytes() + restored = TypedValueBatch.deserialize_from_bytes(payload[:-8] + ending) + assert restored.batch.equals(values.batch, check_metadata=True) + + +@pytest.mark.parametrize("length", range(1, 8)) +def test_truncated_modern_eos_is_a_value_error(length: int) -> None: + payload = TypedValueBatch(pa.record_batch({"x": [1]})).serialize_to_bytes() + with pytest.raises(ValueError): + TypedValueBatch.deserialize_from_bytes(payload[:-8] + payload[-8:][:length]) + + +@pytest.mark.parametrize("length", range(1, 4)) +def test_truncated_legacy_eos_is_a_value_error(length: int) -> None: + payload = TypedValueBatch(pa.record_batch({"x": [1]})).serialize_to_bytes() + with pytest.raises(ValueError): + TypedValueBatch.deserialize_from_bytes(payload[:-8] + b"\0" * length) + + +def test_truncated_body_is_a_value_error() -> None: + payload = TypedValueBatch(pa.record_batch({"x": [1]})).serialize_to_bytes() + with pytest.raises(ValueError): + TypedValueBatch.deserialize_from_bytes(payload[:-9]) + + +@pytest.mark.parametrize("names", [["x", "x"], [""], ["a\x00b"]]) +def test_ambiguous_field_names_reject(names: list[str]) -> None: + batch = pa.RecordBatch.from_arrays([pa.array([1])] * len(names), names=names) + with pytest.raises(ValueError, match="field names"): + TypedValueBatch(batch) + + +@pytest.mark.parametrize("rows", [[], [1, 2]]) +def test_requires_exactly_one_row(rows: list[int]) -> None: + with pytest.raises(ValueError, match="exactly one row"): + TypedValueBatch(pa.record_batch({"x": rows})) + + +@pytest.mark.parametrize("kind", ["missing", "extra", "trailing", "concatenated"]) +def test_rejects_incomplete_or_multiple_payloads(kind: str) -> None: + batch = pa.record_batch({"x": [1]}) + payload = _ipc(batch.schema, batch) + if kind == "missing": + payload = _ipc(batch.schema) + elif kind == "extra": + payload = _ipc(batch.schema, batch, batch) + elif kind == "trailing": + payload += b"ignored?" + else: + payload += payload + with pytest.raises(ValueError, match="no batch|exactly one batch|trailing data"): + TypedValueBatch.deserialize_from_bytes(payload) + + +@pytest.mark.parametrize( + "arrow_type, value", + [ + (pa.int64(), None), + (pa.struct([pa.field("child", pa.int64(), nullable=False)]), {"child": None}), + (pa.list_(pa.field("item", pa.int64(), nullable=False)), [None]), + (pa.large_list(pa.field("item", pa.int64(), nullable=False)), [None]), + (pa.list_(pa.field("item", pa.int64(), nullable=False), 1), [None]), + (pa.list_view(pa.field("item", pa.int64(), nullable=False)), [None]), + (pa.large_list_view(pa.field("item", pa.int64(), nullable=False)), [None]), + # Runtime accepts Field so its nullability survives; PyArrow stubs omit this overload. + ( + cast("Callable[[pa.DataType, pa.Field[pa.DataType]], pa.DataType]", pa.map_)( + pa.string(), pa.field("value", pa.int64(), nullable=False) + ), + [("key", None)], + ), + ], +) +def test_rejects_nulls_in_nonnullable_fields(arrow_type: pa.DataType, value: object) -> None: + schema = pa.schema([pa.field("x", arrow_type, nullable=False)]) + batch = pa.RecordBatch.from_pylist([{"x": value}], schema=schema) + with pytest.raises(ValueError, match="non-nullable"): + TypedValueBatch(batch) + + +def test_null_parent_does_not_require_valid_struct_children() -> None: + schema = pa.schema([pa.field("x", pa.struct([pa.field("child", pa.int64(), nullable=False)]))]) + batch = pa.RecordBatch.from_pylist([{"x": None}], schema=schema) + assert TypedValueBatch.deserialize_from_bytes(_ipc(schema, batch)).batch.equals(batch) + + +@pytest.mark.parametrize("kind", ["dictionary", "extension", "run_end", "union"]) +def test_encoded_values_cannot_hide_nullability_violation(kind: str) -> None: + child = pa.array([None], type=pa.int64()) + array: pa.Array[pa.Scalar[pa.DataType]] + if kind == "dictionary": + array = pa.DictionaryArray.from_arrays(pa.array([0], type=pa.int8()), child) + elif kind == "extension": + array = pa.ExtensionArray.from_storage(pa.bool8(), pa.array([None], type=pa.int8())) + elif kind == "run_end": + array = pa.RunEndEncodedArray.from_arrays(pa.array([1], type=pa.int16()), child) + else: + union_type = pa.dense_union([pa.field("child", pa.int64(), nullable=False)]) + array = pa.Array.from_buffers( + union_type, + 1, + [None, pa.py_buffer(b"\x00"), pa.py_buffer(b"\x00" * 4)], + children=[child], + ) + schema = pa.schema([pa.field("x", array.type, nullable=(kind == "union"))]) + with pytest.raises(ValueError, match="non-nullable"): + TypedValueBatch(pa.RecordBatch.from_arrays([array], schema=schema)) + + +@pytest.mark.parametrize( + "expected", + [ + pa.schema([pa.field("other", pa.int64())]), + pa.schema([pa.field("x", pa.int32())]), + pa.schema([pa.field("x", pa.int64(), nullable=False)]), + pa.schema([pa.field("x", pa.int64(), metadata={b"logical": b"different"})]), + pa.schema([pa.field("x", pa.int64())], metadata={b"context": b"different"}), + ], +) +def test_schema_validation_rejects_rebinding_changes(expected: pa.Schema) -> None: + values = TypedValueBatch(pa.record_batch({"x": [1]})) + with pytest.raises(ValueError, match="expected schema"): + values.validate_schema(expected) + values.validate_schema(pa.record_batch({"x": [2]}).schema) + + +@dataclass(frozen=True, kw_only=True) +class _Envelope(ArrowSerializableDataclass): + values: Annotated[TypedValueBatch, ArrowType(pa.binary())] + + +def test_custom_codec_works_inside_rpc_dataclass() -> None: + values = TypedValueBatch(pa.record_batch({"x": [1]})) + restored = _Envelope.deserialize_from_bytes(_Envelope(values=values).serialize_to_bytes()) + assert restored.values.batch.equals(values.batch, check_metadata=True) + + +def test_custom_codec_rejects_extra_inner_batches() -> None: + """The RPC decoder must call our strict codec, not just read the first batch.""" + inner = pa.record_batch({"x": [1]}) + outer = pa.RecordBatch.from_pylist([{"values": _ipc(inner.schema, inner, inner)}], schema=_Envelope.ARROW_SCHEMA) + with pytest.raises(ValueError, match="exactly one batch"): + _Envelope.deserialize_from_bytes(_ipc(outer.schema, outer)) diff --git a/tests/test_worker_page.py b/tests/test_worker_page.py index f7301091..bad91d84 100644 --- a/tests/test_worker_page.py +++ b/tests/test_worker_page.py @@ -31,6 +31,7 @@ build_worker_page, ) from vgi.metadata import FunctionStability, resolve_metadata +from vgi.protocol import ClientCapabilities from vgi.scalar_function import ScalarFunction from vgi.table_function import TableFunctionGenerator from vgi.table_in_out_function import TableInOutGenerator @@ -438,6 +439,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: Any = None, ) -> CatalogAttachResult: """Stub — describe-page tests don't actually attach.""" @@ -747,6 +749,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: Any = None, ) -> CatalogAttachResult: """Stub — accept any version so the timeline renders cleanly.""" diff --git a/tests/transactor/test_completion_lifecycle.py b/tests/transactor/test_completion_lifecycle.py new file mode 100644 index 00000000..093ac8e9 --- /dev/null +++ b/tests/transactor/test_completion_lifecycle.py @@ -0,0 +1,191 @@ +# Copyright 2026 Query Farm LLC - https://query.farm + +"""Bounded completion and orphan-cleanup tests with real DuckDB transactions. + +Stock DuckDB lacks the bundled transactor's ``enable_suspended_queries`` and +``subcursor()`` extensions. Setup therefore allocates ordinary DuckDB cursors +and installs them in the real transactor's existing registries. No completion, +shutdown, RPC, or engine method is replaced. These tests do not qualify the +bundled BEGIN or streaming-write paths, nor promise timed orphan reclamation. +""" + +from __future__ import annotations + +import socket +import tempfile +import threading +import uuid +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Any + +import pytest +from vgi_rpc.rpc import RpcError, RpcServer, UnixTransport + +from vgi._duckdb import connect +from vgi.transactor.client import TransactorClient +from vgi.transactor.protocol import TransactorProtocol +from vgi.transactor.server import TransactorImpl + + +@pytest.fixture +def transactor(tmp_path: Path) -> Iterator[tuple[TransactorImpl, bytes]]: + """Register a real database and close its cursors even after test failure.""" + implementation = TransactorImpl(str(tmp_path)) + attachment = uuid.uuid4().bytes + implementation.register( + attachment, + ddl_statements=[ + "CREATE TABLE ledger (value BIGINT)", + "INSERT INTO ledger VALUES (10)", + ], + ) + try: + yield implementation, attachment + finally: + with pytest.raises(SystemExit) as exit_info: + implementation.shutdown() + assert exit_info.value.code == 0 + + +def _allocate_cursor(implementation: TransactorImpl, attachment: bytes) -> tuple[bytes, Any]: + """Seed an ordinary transaction without pretending to exercise custom BEGIN.""" + cursor = implementation._get_db_conn(attachment).cursor() + cursor.begin() + transaction = uuid.uuid4().bytes + with implementation._lock: + implementation._transactions.setdefault(attachment, {})[transaction] = cursor + implementation._tx_locks.setdefault(attachment, {})[transaction] = threading.Lock() + return transaction, cursor + + +def _rows(implementation: TransactorImpl, attachment: bytes) -> list[tuple[int]]: + """Read committed data through the independent database connection.""" + return implementation._get_db_conn(attachment).execute("SELECT value FROM ledger ORDER BY value").fetchall() + + +@contextmanager +def _connected_client(implementation: TransactorImpl, monkeypatch: pytest.MonkeyPatch) -> Iterator[TransactorClient]: + """Use the real client and RPC server on one isolated Unix connection.""" + # A short path avoids AF_UNIX's path limit under deeply nested pytest dirs. + with tempfile.TemporaryDirectory(prefix="vgi-tx-") as directory: + socket_path = str(Path(directory) / "rpc.sock") + server = RpcServer(TransactorProtocol, implementation) + failures: list[BaseException] = [] + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + listener.bind(socket_path) + listener.listen(1) + listener.settimeout(5) + + def serve() -> None: + try: + connection, _ = listener.accept() + transport = UnixTransport(connection) + try: + server.serve(transport) + finally: + transport.close() + except BaseException as error: + failures.append(error) + + thread = threading.Thread(target=serve, daemon=True) + thread.start() + monkeypatch.setenv("VGI_TRANSACTOR_SOCKET", socket_path) + client = TransactorClient() + try: + client.get_proxy().ping() + assert client._process is None # Connected to this server; never spawn a shared process. + yield client + finally: + client.close() + thread.join(timeout=5) + assert not thread.is_alive(), "Transactor RPC did not finish after client disconnect" + assert not failures + + +@pytest.mark.parametrize("completion", ["commit", "rollback"]) +@pytest.mark.parametrize("repeated_completion", ["commit", "rollback"]) +def test_completed_transaction_rejects_duplicate_and_opposite_completion_without_effects( + transactor: tuple[TransactorImpl, bytes], + monkeypatch: pytest.MonkeyPatch, + completion: str, + repeated_completion: str, +) -> None: + """Duplicate RPCs cannot reapply writes, undo a commit, or revive a rollback.""" + implementation, attachment = transactor + transaction, cursor = _allocate_cursor(implementation, attachment) + cursor.execute("UPDATE ledger SET value = value + 1") + cursor.execute("INSERT INTO ledger VALUES (20)") + expected = [(11,), (20,)] if completion == "commit" else [(10,)] + + with _connected_client(implementation, monkeypatch) as client: + proxy = client.get_proxy() + getattr(proxy, completion)(attach_opaque_data=attachment, tx_id=transaction) + assert _rows(implementation, attachment) == expected + assert transaction not in implementation._transactions[attachment] + assert transaction not in implementation._tx_locks[attachment] + + # Keep a different transaction active while stale completion calls arrive. + other_transaction, other_cursor = _allocate_cursor(implementation, attachment) + other_cursor.execute("INSERT INTO ledger VALUES (30)") + for _ in range(3): + with pytest.raises(RpcError, match="No active transaction"): + getattr(proxy, repeated_completion)(attach_opaque_data=attachment, tx_id=transaction) + assert _rows(implementation, attachment) == expected + assert implementation._get_tx_conn(attachment, other_transaction) is other_cursor + + proxy.commit(attach_opaque_data=attachment, tx_id=other_transaction) + assert _rows(implementation, attachment) == [*expected, (30,)] + with pytest.raises(ValueError, match="No active transaction"): + implementation.list_user_tables(attachment, transaction) + + +def test_client_disconnect_leaves_abandoned_transaction_active( + transactor: tuple[TransactorImpl, bytes], monkeypatch: pytest.MonkeyPatch +) -> None: + """Connection closure is not transaction cleanup for the shared transactor.""" + implementation, attachment = transactor + transaction, cursor = _allocate_cursor(implementation, attachment) + cursor.execute("INSERT INTO ledger VALUES (99)") + with _connected_client(implementation, monkeypatch) as client: + assert client.get_proxy().list_user_tables(attach_opaque_data=attachment, tx_id=transaction) == ["ledger"] + + # The request connection and its serving thread are gone, but the shared + # provider still owns the transaction. No wall-clock delay implies a lease. + assert implementation._get_tx_conn(attachment, transaction) is cursor + assert cursor.execute("SELECT value FROM ledger ORDER BY value").fetchall() == [(10,), (99,)] + assert _rows(implementation, attachment) == [(10,)] + implementation.rollback(attachment, transaction) + assert _rows(implementation, attachment) == [(10,)] + + +def test_explicit_shutdown_rolls_back_abandoned_transactions_and_releases_databases(tmp_path: Path) -> None: + """Existing explicit shutdown discards orphan DML/DDL in every attachment.""" + implementation = TransactorImpl(str(tmp_path)) + attachments = [uuid.uuid4().bytes, uuid.uuid4().bytes] + cursors = [] + try: + for attachment in attachments: + implementation.register(attachment, ddl_statements=["CREATE TABLE ledger (value BIGINT)"]) + _, cursor = _allocate_cursor(implementation, attachment) + cursors.append(cursor) + cursor.execute("INSERT INTO ledger VALUES (99)") + cursor.execute("CREATE TABLE abandoned (value BIGINT)") + + with pytest.raises(SystemExit) as exit_info: + implementation.shutdown() + assert exit_info.value.code == 0 + assert implementation._transactions == {} + assert implementation._tx_locks == {} + assert implementation._databases == {} + for attachment in attachments: + with connect(str(tmp_path / f"{attachment.hex()}.duckdb")) as reopened: + assert reopened.execute("SELECT * FROM ledger").fetchall() == [] + assert reopened.execute("SHOW TABLES").fetchall() == [("ledger",)] + for cursor in cursors: + with pytest.raises(Exception, match="closed"): + cursor.execute("SELECT 1") + finally: + with pytest.raises(SystemExit): + implementation.shutdown() diff --git a/tests/transactor/test_protocol_registration.py b/tests/transactor/test_protocol_registration.py new file mode 100644 index 00000000..029b613c --- /dev/null +++ b/tests/transactor/test_protocol_registration.py @@ -0,0 +1,15 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""The transactor's typed write-result modes must permit RPC server startup.""" + +from pathlib import Path + +from vgi_rpc.rpc import RpcServer + +from vgi.transactor.protocol import TransactorProtocol +from vgi.transactor.server import TransactorImpl + + +def test_typed_write_modes_allow_protocol_registration(tmp_path: Path) -> None: + """Register the real interface, including INSERT/UPDATE/DELETE signatures.""" + RpcServer(TransactorProtocol, TransactorImpl(str(tmp_path))) diff --git a/uv.lock b/uv.lock index e80e3f98..941bfd3b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.15'", @@ -2733,7 +2733,7 @@ wheels = [ [[package]] name = "vgi-fixtures" -version = "0.47.0" +version = "0.48.0" source = { editable = "packages/vgi-fixtures" } dependencies = [ { name = "vgi-python", extra = ["duckdb", "test-fixtures-writable"] }, @@ -2749,7 +2749,7 @@ requires-dist = [ [[package]] name = "vgi-python" -version = "0.47.0" +version = "0.48.0" source = { editable = "." } dependencies = [ { name = "click" }, @@ -2872,7 +2872,7 @@ requires-dist = [ { name = "vgi-python", extras = ["test-fixtures"], marker = "extra == 'test-fixtures-writable'" }, { name = "vgi-python", extras = ["test-fixtures", "test-fixtures-writable", "http", "launch", "oauth", "otel", "sentry", "azure"], marker = "extra == 'dev'" }, { name = "vgi-python", extras = ["transactor"], marker = "extra == 'test-fixtures-writable'" }, - { name = "vgi-rpc", specifier = ">=0.52.0" }, + { name = "vgi-rpc", specifier = ">=0.52.3" }, { name = "vgi-rpc", extras = ["cli"], marker = "extra == 'launch'" }, { name = "vgi-rpc", extras = ["cli", "conformance", "external", "http", "oauth", "otel", "sentry"], marker = "extra == 'dev'" }, { name = "vgi-rpc", extras = ["external"], marker = "extra == 'external'" }, @@ -2904,7 +2904,7 @@ docs = [ [[package]] name = "vgi-rpc" -version = "0.52.0" +version = "0.52.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -2912,9 +2912,9 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/8b/0a4aefb96838c0e413c93afbd1c177aa3ebfaa22b134fd25b529d8763fee/vgi_rpc-0.52.0.tar.gz", hash = "sha256:fa94674f134ebce67662d803b5855a443057698d58d6ae90fa145b1bbb19f58d", size = 1636721, upload-time = "2026-10-20T14:20:29.216Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/8e/de0ccdf31680e54aa35d36fdd6b7051beb34a8c676e4eca8db9f1907695a/vgi_rpc-0.52.3.tar.gz", hash = "sha256:2babfaeaac1ae2cdd314a1401dc133272ee834a062d930a84af2a999edf86f79", size = 1642210, upload-time = "2026-10-27T19:20:13.586Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/d9/88e65dd7b63a5bdadc6e1dd5921f3dc9cc36d03f3a8cce5d35cdb514211a/vgi_rpc-0.52.0-py3-none-any.whl", hash = "sha256:6a468b957cd8a9fbd78682dc093c02dba9392c7a2369e6c2cd663eb331098ee0", size = 761773, upload-time = "2026-10-20T14:20:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/eef4739b2292e0e9bdeb1f788e4e1ab6ced863b2564f2b5c3c22fb6b60dc/vgi_rpc-0.52.3-py3-none-any.whl", hash = "sha256:c1d71f126901cc5afce9f515fdfa8c9c487faf6003a6a59bbcdec3ba18a997af", size = 762552, upload-time = "2026-10-27T19:20:11.875Z" }, ] [package.optional-dependencies] diff --git a/vgi/__init__.py b/vgi/__init__.py index 4203764e..23b5878a 100644 --- a/vgi/__init__.py +++ b/vgi/__init__.py @@ -79,6 +79,7 @@ type_payload, value_payload, ) +from vgi.invocation import BindRepeatability from vgi.metadata import ( ArgumentMonotonicity, CatalogFunctionType, @@ -99,6 +100,8 @@ from vgi.protocol import ( BindRequest, InitRequest, + TableFunctionProjectionRequest, + TableFunctionProjectionResponse, VgiOutputCollector, ) from vgi.scalar_function import ( @@ -132,6 +135,8 @@ from vgi.write_results import WriteOperation, WriteResultMode, write_changes_batch, write_result_schema __all__ = [ + "TableFunctionProjectionRequest", + "TableFunctionProjectionResponse", "AggregateBindParams", "AggregateFunction", "WriteOperation", @@ -151,6 +156,7 @@ "argument_specs_to_schema", "parameter_default_values_from_specs", "BindRequest", + "BindRepeatability", "ColumnBounds", "ColumnRefNode", "ComparisonNode", diff --git a/vgi/_test_fixtures/accumulate/worker.py b/vgi/_test_fixtures/accumulate/worker.py index 168d3e5c..d29b844e 100644 --- a/vgi/_test_fixtures/accumulate/worker.py +++ b/vgi/_test_fixtures/accumulate/worker.py @@ -69,6 +69,7 @@ from vgi.function_storage import BoundStorage, FunctionStorage from vgi.invocation import BindResponse from vgi.metadata import FunctionExample +from vgi.protocol import ClientCapabilities from vgi.table_buffering_function import TableBufferingFunction, TableBufferingParams from vgi.table_function import ( BindParams, @@ -734,6 +735,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: CallContext | None = None, ) -> CatalogAttachResult: """Attach, minting a random per-ATTACH storage scope id.""" @@ -742,6 +744,7 @@ def catalog_attach( options=options, data_version_spec=data_version_spec, implementation_version=implementation_version, + client_capabilities=client_capabilities, ctx=ctx, ) return dataclasses.replace( diff --git a/vgi/_test_fixtures/attach_options.py b/vgi/_test_fixtures/attach_options.py index dc28fad8..5bde2867 100644 --- a/vgi/_test_fixtures/attach_options.py +++ b/vgi/_test_fixtures/attach_options.py @@ -46,6 +46,7 @@ ) from vgi.catalog.descriptors import Catalog, Schema from vgi.invocation import BindResponse +from vgi.protocol import ClientCapabilities from vgi.schema_utils import schema from vgi.table_function import BindParams, ProcessParams, TableFunctionGenerator, init_single_worker from vgi.worker import Worker @@ -260,6 +261,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: CallContext | None = None, ) -> CatalogAttachResult: del data_version_spec, implementation_version, ctx diff --git a/vgi/_test_fixtures/bad_protocol.py b/vgi/_test_fixtures/bad_protocol.py index 88511e28..c2c7e415 100644 --- a/vgi/_test_fixtures/bad_protocol.py +++ b/vgi/_test_fixtures/bad_protocol.py @@ -51,7 +51,7 @@ class BadProtocol(VgiProtocol): # finds no such protocol hosted, and the caller gets # ``ProtocolNotSupportedError`` before the version gate it came to test # ever runs. - protocol_name: ClassVar[str] = "vgi.v2" + protocol_name: ClassVar[str] = "vgi.v3" protocol_version: ClassVar[str] = BAD_PROTOCOL_VERSION diff --git a/vgi/_test_fixtures/catalog.py b/vgi/_test_fixtures/catalog.py index f7537009..47da4a3e 100644 --- a/vgi/_test_fixtures/catalog.py +++ b/vgi/_test_fixtures/catalog.py @@ -8,13 +8,13 @@ from __future__ import annotations +import dataclasses import uuid from collections.abc import Sequence from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal, overload if TYPE_CHECKING: - import pyarrow as pa from vgi_rpc.rpc import CallContext from vgi._redact import short_hash @@ -35,7 +35,11 @@ TransactionOpaqueData, ViewInfo, ) +from vgi.catalog.layout import TableLayout +from vgi.catalog.macros import MacroOverload +from vgi.protocol import ClientCapabilities from vgi.schema_path import SchemaKey, schema_path_key +from vgi.typed_values import TypedValueBatch from vgi.worker import Worker @@ -154,6 +158,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: CallContext | None = None, ) -> CatalogAttachResult: """Attach to a catalog with the given name. @@ -354,19 +359,27 @@ def schema_create( on_conflict: OnConflict = OnConflict.ERROR, comment: str | None, tags: dict[str, str], + options: TypedValueBatch | None = None, ) -> None: """Create a new schema.""" + if options is not None: + raise NotImplementedError("Schema options are not supported by this fixture") catalog = self._get_catalog(attach_opaque_data) path_key = schema_path_key(path) if path_key in catalog.schemas: - msg = f"Schema {path!r} already exists" - raise ValueError(msg) + if on_conflict == OnConflict.IGNORE: + return + if on_conflict != OnConflict.REPLACE: + msg = f"Schema {path!r} already exists" + raise ValueError(msg) + if len(path) > 1 and schema_path_key(path[:-1]) not in catalog.schemas: + raise ValueError("Parent schema does not exist") catalog.schemas[path_key] = SchemaData( info=SchemaInfo( attach_opaque_data=attach_opaque_data, path=list(path), comment=comment, - tags=tags, + tags=dict(tags), ) ) self._increment_version(attach_opaque_data) @@ -507,13 +520,18 @@ def table_create( name: str, columns: SerializedSchema, on_conflict: OnConflict, - not_null_constraints: list[int], - unique_constraints: list[list[int]], - check_constraints: list[str], - primary_key_constraints: list[list[int]] | None = None, - foreign_key_constraints: list[bytes] | None = None, + constraint_definitions: list[bytes], + comment: str | None = None, + tags: dict[str, str] | None = None, + options: TypedValueBatch | None = None, + layout: TableLayout | None = None, ) -> None: """Create a new table.""" + if constraint_definitions: + raise NotImplementedError("this metadata-only catalog does not enforce canonical constraints") + if any(value is not None for value in (comment, tags, options, layout)): + raise NotImplementedError("table creation metadata/options/layout profiles are not supported") + schema_data = self._get_schema(attach_opaque_data, schema_path) if name in schema_data.tables: if on_conflict == OnConflict.ERROR: @@ -528,9 +546,7 @@ def table_create( name=name, schema_path=schema_path, columns=columns, - not_null_constraints=not_null_constraints, - unique_constraints=unique_constraints, - check_constraints=check_constraints, + constraints=[], comment=None, tags={}, ) @@ -582,9 +598,8 @@ def table_comment_set( name=old_info.name, schema_path=old_info.schema_path, columns=old_info.columns, - not_null_constraints=old_info.not_null_constraints, - unique_constraints=old_info.unique_constraints, - check_constraints=old_info.check_constraints, + constraints=old_info.constraints, + definition_revision=old_info.definition_revision, comment=comment, tags=old_info.tags, ) @@ -619,9 +634,8 @@ def table_rename( name=new_name, schema_path=old_info.schema_path, columns=old_info.columns, - not_null_constraints=old_info.not_null_constraints, - unique_constraints=old_info.unique_constraints, - check_constraints=old_info.check_constraints, + constraints=old_info.constraints, + definition_revision=old_info.definition_revision, comment=old_info.comment, tags=old_info.tags, ) @@ -637,8 +651,15 @@ def view_create( name: str, definition: str, on_conflict: OnConflict, + aliases: list[str] | None = None, + security: str = "regular", + comment: str | None = None, + tags: dict[str, str] | None = None, + column_comments: dict[str, str] | None = None, ) -> None: """Create a new view.""" + if security != "regular": + raise NotImplementedError("view security mode is unsupported by this provider") schema_data = self._get_schema(attach_opaque_data, schema_path) if name in schema_data.views: if on_conflict == OnConflict.ERROR: @@ -653,8 +674,10 @@ def view_create( name=name, schema_path=schema_path, definition=definition, - comment=None, - tags={}, + aliases=list(aliases or []), + comment=comment, + tags=dict(tags or {}), + column_comments=dict(column_comments or {}), ) ) self._increment_version(attach_opaque_data) @@ -707,6 +730,9 @@ def view_rename( name=new_name, schema_path=old_info.schema_path, definition=old_info.definition, + aliases=list(old_info.aliases), + security=old_info.security, + column_comments=dict(old_info.column_comments), comment=old_info.comment, tags=old_info.tags, ) @@ -738,12 +764,43 @@ def view_comment_set( name=old_info.name, schema_path=old_info.schema_path, definition=old_info.definition, + aliases=list(old_info.aliases), + security=old_info.security, + column_comments=dict(old_info.column_comments), comment=comment, tags=old_info.tags, ) ) self._increment_version(attach_opaque_data) + def view_column_comment_set( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: list[str], + name: str, + column_path: list[str], + comment: str | None, + ignore_not_found: bool, + ) -> None: + """Set or clear the comment keyed by a raw view output name.""" + if len(column_path) != 1 or not column_path[0] or "\0" in column_path[0]: + raise ValueError("view column comments require one raw output identifier") + schema_data = self._get_schema(attach_opaque_data, schema_path) + view = schema_data.views.get(name) + if view is None: + if ignore_not_found: + return + raise ValueError(f"View {name!r} not found in schema {schema_path!r}") + comments = dict(view.info.column_comments) + if comment is None: + comments.pop(column_path[0], None) + else: + comments[column_path[0]] = comment + schema_data.views[name] = ViewData(info=dataclasses.replace(view.info, column_comments=comments)) + self._increment_version(attach_opaque_data) + def macro_create( self, *, @@ -752,11 +809,8 @@ def macro_create( schema_path: list[str], name: str, macro_type: MacroType, - parameters: list[str], - definition: str, + overloads: list[MacroOverload], on_conflict: OnConflict, - parameter_default_values: pa.RecordBatch | None = None, - arguments_schema: pa.Schema | None = None, ) -> None: """Create a new macro.""" schema_data = self._get_schema(attach_opaque_data, schema_path) @@ -773,12 +827,9 @@ def macro_create( name=name, schema_path=schema_path, macro_type=macro_type, - parameters=parameters, - parameter_default_values=parameter_default_values, - definition=definition, + overloads=overloads, comment=None, tags={}, - arguments_schema=arguments_schema, ) ) self._increment_version(attach_opaque_data) diff --git a/vgi/_test_fixtures/catalog_contents.py b/vgi/_test_fixtures/catalog_contents.py index 89dab964..13ae7b8d 100644 --- a/vgi/_test_fixtures/catalog_contents.py +++ b/vgi/_test_fixtures/catalog_contents.py @@ -78,6 +78,7 @@ Table, View, ) +from vgi.protocol import ClientCapabilities from vgi.worker import Worker CATALOG_PROBE = "contents_probe" @@ -232,6 +233,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: Any = None, ) -> CatalogAttachResult: """Attach a fresh private catalog, advertising ``catalog_contents``.""" @@ -248,6 +250,7 @@ def catalog_attach( options=options, data_version_spec=data_version_spec, implementation_version=implementation_version, + client_capabilities=client_capabilities, ctx=ctx, ) return replace( diff --git a/vgi/_test_fixtures/constraint_facts.py b/vgi/_test_fixtures/constraint_facts.py new file mode 100644 index 00000000..17de9b8c --- /dev/null +++ b/vgi/_test_fixtures/constraint_facts.py @@ -0,0 +1,45 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Explicit facts for fixed fixture tables; never an inference from Arrow flags. + +Callers own the proof: immutable rows are checked before publication, or every +fixture storage write validates values. Keys are fixed provider-owned identifiers +for tables whose definitions cannot be dropped/recreated through their API. +""" + +from typing import Any + +from vgi.catalog.constraints import ( + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintValidation, +) +from vgi.catalog.dependencies import CatalogObjectRef + + +def fixed_fact( + table: str, path: list[str], key: str, definition: ConstraintDefinition, **kwargs: Any +) -> ConstraintInfo: + """Attach an explicit fixture definition to its stable provider-owned key.""" + return ConstraintInfo( + ref=CatalogObjectRef(kind="constraint", schema_path=path, owner_table=table, name=key), + definition=definition, + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + **kwargs, + ) + + +def fixed_not_null(table: str, path: list[str], columns: tuple[tuple[str, ...], ...]) -> tuple[ConstraintInfo, ...]: + """Encode explicit NOT_NULL facts that the fixed fixture actually enforces.""" + return tuple( + fixed_fact( + table, + path, + f"not-null-{index}", + ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[list(column)]), + ) + for index, column in enumerate(columns) + ) diff --git a/vgi/_test_fixtures/http_server.py b/vgi/_test_fixtures/http_server.py index ee3e3ede..0c06322b 100644 --- a/vgi/_test_fixtures/http_server.py +++ b/vgi/_test_fixtures/http_server.py @@ -310,16 +310,6 @@ def _make_compression() -> Compression | None: ) sys.stderr.flush() - import socket - - if port == 0: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind((host, 0)) - port = int(s.getsockname()[1]) - - if demo_storage: - demo_blob_storage.set_base_url(f"http://{host}:{port}") - from vgi.serve import _resolve_authenticate, _resolve_oauth_resource_metadata authenticate = _resolve_authenticate() @@ -487,6 +477,15 @@ def _optional_bearer(req: Any) -> AuthContext: # we serve the Falcon app directly. serving_app: Any = wsgi_app + # Keep the selected socket bound while publishing its port. Selecting + # and releasing a free port first lets another test worker take it. + # Auto-selection has always used IPv4; retain one listener for port 0. + http_server = waitress.create_server(serving_app, host=host, port=port, ipv6=port != 0) + if port == 0: + port = int(http_server.effective_port) + if demo_storage: + demo_blob_storage.set_base_url(f"http://{host}:{port}") + if port_file is not None: # Atomic side-channel publication so test harnesses can watch # for the file without racing a partial write. Same helper the @@ -498,7 +497,7 @@ def _optional_bearer(req: Any) -> AuthContext: print(f"PORT:{port}", flush=True) sys.stderr.write(f"Serving ExampleWorker on http://{host}:{port}{prefix}\n") sys.stderr.flush() - waitress.serve(serving_app, host=host, port=port, _quiet=True) + http_server.run() app() diff --git a/vgi/_test_fixtures/narrow_bind/worker.py b/vgi/_test_fixtures/narrow_bind/worker.py index 2b43be97..95eddcad 100644 --- a/vgi/_test_fixtures/narrow_bind/worker.py +++ b/vgi/_test_fixtures/narrow_bind/worker.py @@ -156,9 +156,7 @@ def _info(self, table_name: str) -> TableInfo: name=table_name, schema_path=["main"], columns=SerializedSchema(_serialize_schema(_TABLE_SCHEMA)), - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], ) def schemas( diff --git a/vgi/_test_fixtures/projection_repro/worker.py b/vgi/_test_fixtures/projection_repro/worker.py index e9ab7b4f..37d6bafe 100644 --- a/vgi/_test_fixtures/projection_repro/worker.py +++ b/vgi/_test_fixtures/projection_repro/worker.py @@ -42,8 +42,10 @@ from vgi_rpc.rpc import OutputCollector from vgi import Worker +from vgi._test_fixtures.constraint_facts import fixed_not_null from vgi.arguments import Arg from vgi.catalog import Catalog, Schema +from vgi.catalog.capabilities import CONSTRAINT_RECORDS_READ, CatalogCapabilities from vgi.catalog.catalog_interface import ( AttachOpaqueData, ReadOnlyCatalogInterface, @@ -357,6 +359,10 @@ class ProjReproCatalog(ReadOnlyCatalogInterface): catalog = _CATALOG catalog_name = CATALOG_NAME + def catalog_capabilities(self, *, attach_opaque_data: AttachOpaqueData) -> CatalogCapabilities: + """The generator always supplies these four fields and exposes no writes.""" + return CatalogCapabilities(read=[CONSTRAINT_RECORDS_READ]) + def _info(self, table_name: str) -> TableInfo: return TableInfo( comment=f"reproducer table -> {_TABLE_NAMES[table_name]}", @@ -364,9 +370,12 @@ def _info(self, table_name: str) -> TableInfo: name=table_name, schema_path=["main"], columns=SerializedSchema(_serialize_schema(WIDE_SCHEMA)), - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[ + fact.serialize_to_bytes() + for fact in fixed_not_null( + table_name, ["main"], (("topic",), ("partition",), ("offset",), ("headers",)) + ) + ], ) def schemas( diff --git a/vgi/_test_fixtures/schema_reconcile/worker.py b/vgi/_test_fixtures/schema_reconcile/worker.py index 0193c0dd..a25a4287 100644 --- a/vgi/_test_fixtures/schema_reconcile/worker.py +++ b/vgi/_test_fixtures/schema_reconcile/worker.py @@ -37,7 +37,9 @@ from vgi_rpc.rpc import OutputCollector from vgi import Worker +from vgi._test_fixtures.constraint_facts import fixed_not_null from vgi.catalog import Catalog, Schema +from vgi.catalog.capabilities import CONSTRAINT_ENFORCE, CONSTRAINT_RECORDS_READ, CatalogCapabilities from vgi.catalog.catalog_interface import ( AttachOpaqueData, ReadOnlyCatalogInterface, @@ -56,6 +58,7 @@ TableFunctionGenerator, ) from vgi.table_in_out_function import TableInOutGenerator +from vgi.typed_values import TypedValueBatch CATALOG_NAME = "schema_reconcile" _SCHEMA_NAME = "main" @@ -217,6 +220,7 @@ def _all_rows(spec: TableSpec) -> list[tuple[Any, dict[str, Any]]]: def _insert_row(spec: TableSpec, rid: Any, payload: dict[str, Any]) -> None: + TypedValueBatch(pa.RecordBatch.from_pylist([{**payload, "rowid": rid}], schema=spec.table_schema)) with _lock, _connect() as conn: conn.execute( f"INSERT OR REPLACE INTO {spec.storage_table} (rid_blob, payload) VALUES (?, ?)", @@ -234,6 +238,7 @@ def _update_row(spec: TableSpec, rid: Any, updates: dict[str, Any]) -> bool: return False payload = pickle.loads(row[0]) payload.update(updates) + TypedValueBatch(pa.RecordBatch.from_pylist([{**payload, "rowid": rid}], schema=spec.table_schema)) conn.execute( f"UPDATE {spec.storage_table} SET payload = ? WHERE rid_blob = ?", (pickle.dumps(payload), _rid_key(rid)), @@ -518,16 +523,27 @@ class SchemaReconcileCatalog(ReadOnlyCatalogInterface): catalog = _CATALOG catalog_name = CATALOG_NAME + def catalog_capabilities(self, *, attach_opaque_data: AttachOpaqueData) -> CatalogCapabilities: + """Every fixture insert/update checks required values before publication.""" + return CatalogCapabilities(read=[CONSTRAINT_RECORDS_READ], write=[CONSTRAINT_ENFORCE]) + def _table_info(self, spec: TableSpec) -> TableInfo: + # Qualify ALL_ROWS for storage created by a previous fixture process, + # as well as the writes validated by the current implementation. + for rid, payload in _all_rows(spec): + TypedValueBatch(pa.RecordBatch.from_pylist([{**payload, "rowid": rid}], schema=spec.table_schema)) return TableInfo( comment=f"Schema-reconcile {spec.name} (rowid type {spec.rowid_field.type})", tags={}, name=spec.name, schema_path=[_SCHEMA_NAME], columns=SerializedSchema(_serialize_schema(spec.table_schema)), - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[ + fact.serialize_to_bytes() + for fact in fixed_not_null( + spec.name, [_SCHEMA_NAME], (("id",), ("ts",), ("nested",), ("nested", "a"), ("tags",)) + ) + ], write_result_modes={"insert": "changes", "update": "changes", "delete": "changes"}, ) diff --git a/vgi/_test_fixtures/simple_writable.py b/vgi/_test_fixtures/simple_writable.py index 8afa71d6..80c2aea7 100644 --- a/vgi/_test_fixtures/simple_writable.py +++ b/vgi/_test_fixtures/simple_writable.py @@ -56,6 +56,7 @@ ) from vgi.catalog.descriptors import Catalog, Schema from vgi.invocation import BindResponse, GlobalInitResponse +from vgi.protocol import ClientCapabilities from vgi.schema_utils import schema as build_schema from vgi.table_function import BindParams, InitParams, ProcessParams, TableFunctionGenerator from vgi.table_in_out_function import TableInOutGenerator @@ -336,6 +337,7 @@ def process(cls, params: ProcessParams[None], state: _ScanState, out: OutputColl state.cursor = len(state.rows) arrow_arrays = [pa.array(col, type=state.schema.field(i).type) for i, col in enumerate(col_arrays)] out.emit(pa.RecordBatch.from_arrays(arrow_arrays, schema=state.schema)) + out.finish() @dataclass(kw_only=True) @@ -599,6 +601,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: Any | None = None, ) -> CatalogAttachResult: del options, data_version_spec, implementation_version, ctx @@ -653,11 +656,7 @@ def _build_table_info(self, *, name: str, schema_path: list[str]) -> TableInfo: name=name, schema_path=schema_path, columns=SerializedSchema(full.serialize().to_pybytes()), - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], - primary_key_constraints=[], - foreign_key_constraints=[], + constraints=[], write_result_modes={ operation: _table_result_mode(name) for operation, supported in (("insert", True), ("update", ud), ("delete", ud)) diff --git a/vgi/_test_fixtures/table/catalog_scans.py b/vgi/_test_fixtures/table/catalog_scans.py index a626cb1a..751cd46b 100644 --- a/vgi/_test_fixtures/table/catalog_scans.py +++ b/vgi/_test_fixtures/table/catalog_scans.py @@ -18,6 +18,8 @@ init_single_worker, ) +STATIC_CATALOG_ROWS: dict[str, pa.Table] = {} + def _static_scan_function( func_name: str, @@ -31,6 +33,7 @@ def _static_scan_function( return a fixed dataset. Each generated class is decorated with ``@init_single_worker`` and has a unique ``Meta.name``. """ + STATIC_CATALOG_ROWS[func_name] = pa.Table.from_pydict(data, schema=output_schema) @init_single_worker class StaticScanFunction(TableFunctionGenerator[_EmptyArgs, _OneShotState]): diff --git a/vgi/_test_fixtures/table/splits.py b/vgi/_test_fixtures/table/splits.py index fad8768d..96e2f447 100644 --- a/vgi/_test_fixtures/table/splits.py +++ b/vgi/_test_fixtures/table/splits.py @@ -1113,6 +1113,12 @@ def _render_filters_canonical(filters: PushdownFilters | None) -> str: for c in columns: bounds = filters.get_column_bounds(c) if bounds is None: + # DuckDB 2.0 can send an exact IN set without redundant range + # predicates. Render its integer envelope in the same canonical form. + values = filters.get_column_values(c) + discrete = [] if values is None else [_bound_as_int(value) for value in values if value.is_valid] + if discrete: + parts.extend((f"{c}>={min(discrete)}", f"{c}<={max(discrete)}")) continue # Normalized to INCLUSIVE integer bounds, because that is the only form # every SDK can produce: Rust's ColumnBounds is integer-coerced and diff --git a/vgi/_test_fixtures/ticket_probe.py b/vgi/_test_fixtures/ticket_probe.py index 34a8faf4..8cb4a4bc 100644 --- a/vgi/_test_fixtures/ticket_probe.py +++ b/vgi/_test_fixtures/ticket_probe.py @@ -39,6 +39,7 @@ from vgi.catalog import AttachOpaqueData, Catalog, CatalogAttachResult, ReadOnlyCatalogInterface, Schema, Table from vgi.catalog.attach_option import AttachOption from vgi.invocation import BindResponse +from vgi.protocol import ClientCapabilities from vgi.table_function import BindParams, ProcessParams, TableFunctionGenerator, init_single_worker from vgi.worker import Worker @@ -139,6 +140,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: CallContext | None = None, ) -> CatalogAttachResult: """Attach, recording ``region`` and ``sha256(api_key)[:12]`` (never the key).""" @@ -147,6 +149,7 @@ def catalog_attach( options=options, data_version_spec=data_version_spec, implementation_version=implementation_version, + client_capabilities=client_capabilities, ctx=ctx, ) lowered = {key.lower(): value for key, value in options.items()} diff --git a/vgi/_test_fixtures/versioned.py b/vgi/_test_fixtures/versioned.py index 189fafb9..fdb6ccc3 100644 --- a/vgi/_test_fixtures/versioned.py +++ b/vgi/_test_fixtures/versioned.py @@ -34,6 +34,7 @@ TransactionOpaqueData, ) from vgi.catalog.descriptors import Catalog, Schema +from vgi.protocol import ClientCapabilities from vgi.worker import Worker if TYPE_CHECKING: @@ -78,6 +79,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: CallContext | None = None, ) -> CatalogAttachResult: """Validate requested versions and pin an HTTP session.""" diff --git a/vgi/_test_fixtures/versioned_tables.py b/vgi/_test_fixtures/versioned_tables.py index bbd7b32a..9b537d72 100644 --- a/vgi/_test_fixtures/versioned_tables.py +++ b/vgi/_test_fixtures/versioned_tables.py @@ -52,6 +52,7 @@ TransactionOpaqueData, ) from vgi.invocation import BindResponse +from vgi.protocol import ClientCapabilities from vgi.schema_utils import schema from vgi.table_function import BindParams, ProcessParams, TableFunctionGenerator, init_single_worker from vgi.worker import Worker @@ -411,6 +412,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: CallContext | None = None, ) -> CatalogAttachResult: """Validate versions, record the resolved data version, return attach result.""" @@ -458,9 +460,7 @@ def _make_table_info(name: str, table: _VersionedTable) -> TableInfo: name=name, schema_path=["main"], columns=SerializedSchema(table.columns.serialize().to_pybytes()), - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], ) # ------------------------------------------------------------------ schemas / tables diff --git a/vgi/_test_fixtures/worker.py b/vgi/_test_fixtures/worker.py index da9ef29d..bb91def1 100644 --- a/vgi/_test_fixtures/worker.py +++ b/vgi/_test_fixtures/worker.py @@ -65,6 +65,7 @@ SlowCancellableFunction, SlowCancellableInOutFunction, ) +from vgi._test_fixtures.constraint_facts import fixed_fact from vgi._test_fixtures.copy_from import ExampleLinesCopyFromFunction, SecretLinesCopyFromFunction from vgi._test_fixtures.copy_to import ( ExampleLinesCopyToFunction, @@ -137,6 +138,7 @@ SecretCacheNonceFunction, ) from vgi._test_fixtures.table import ( + _VERSIONED_CONSTRAINTS_DATA, _VERSIONED_CONSTRAINTS_SCHEMAS, _VERSIONED_SCHEMAS, RFF_MULTI_COLUMNS, @@ -259,6 +261,7 @@ resolve_version, resolve_versioned_constraints_version, ) +from vgi._test_fixtures.table.catalog_scans import STATIC_CATALOG_ROWS from vgi._test_fixtures.table.same_name_cached import ( SameNameDataCached, SameNameMainCached, @@ -325,7 +328,6 @@ from vgi.catalog import ( AttachOpaqueData, Catalog, - ForeignKeyDef, Index, IndexConstraintType, Macro, @@ -343,10 +345,23 @@ TransactionOpaqueData, View, ) +from vgi.catalog.capabilities import ( + CONSTRAINT_RECORDS_READ, + INDEX_DEFINITION_READ, + ROW_SCALAR_EXPRESSION, + CatalogCapabilities, +) from vgi.catalog.catalog_interface import _validate_at_params +from vgi.catalog.constraints import ConstraintDefinition, ConstraintInfo, ConstraintKind, ForeignKeyDefinition +from vgi.catalog.dependencies import CatalogObjectRef from vgi.catalog.descriptors import ColumnStatisticsInput from vgi.catalog.duckdb_statistics import statistics_from_duckdb +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.indexes import IndexDefinition, IndexKey +from vgi.catalog.options import OptionState +from vgi.catalog.sql import SqlExpression as CanonicalSqlExpression from vgi.schema_utils import schema +from vgi.typed_values import TypedValueBatch from vgi.worker import Worker # Shared scratch dir for native-branch fixtures (read_parquet / read_csv arms). @@ -414,6 +429,109 @@ def _build_fixture_stats() -> tuple[ _NUMBERS_STATS, _GEO_SCHEMA, _GEO_STATS, _ENUM_STATS = _build_fixture_stats() + +def _fixture_constraints(name: str, version: int = 3) -> tuple[ConstraintInfo, ...]: + """Qualify explicit constraints against the fixture's entire immutable dataset.""" + not_null = { + "departments": ("id", "name"), + "products": ("id",), + "employees": ("id", "name", "email"), + "projects": ("department_id", "project_code", "title"), + "versioned_constraints": ("id",) if version == 1 else ("id", "name"), + }[name] + values = ( + pa.Table.from_pydict(_VERSIONED_CONSTRAINTS_DATA[version]).to_pylist() + if name == "versioned_constraints" + else STATIC_CATALOG_ROWS[f"{name}_scan"].to_pylist() + ) + definitions = [ + (f"not-null-{column}", ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[[column]])) + for column in not_null + ] + if name != "versioned_constraints" or version >= 2: + primary = ["department_id", "project_code"] if name == "projects" else ["id"] + definitions.append( + ("primary-key", ConstraintDefinition(kind=ConstraintKind.PRIMARY_KEY, column_paths=[[c] for c in primary])) + ) + unique = ( + "name" + if name == "departments" + else "email" + if name in {"employees", "versioned_constraints"} and version >= 2 + else None + ) + if unique is not None: + definitions.append(("unique-value", ConstraintDefinition(kind=ConstraintKind.UNIQUE, column_paths=[[unique]]))) + if name == "departments": + definitions.append( + ( + "nonnegative-budget", + ConstraintDefinition( + kind=ConstraintKind.CHECK, + check_expression=CanonicalSqlExpression( + sql="budget >= 0", + profile=ROW_SCALAR_EXPRESSION, + schema_path=["data"], + evaluation_context=QueryEvaluationContext(), + ), + ), + ) + ) + has_foreign = name in {"employees", "projects"} or name == "versioned_constraints" and version >= 3 + if has_foreign: + definitions.append( + ( + "department-reference", + ConstraintDefinition( + kind=ConstraintKind.FOREIGN_KEY, + column_paths=[["department_id"]], + foreign_key=ForeignKeyDefinition( + referenced_table=CatalogObjectRef(kind="table", schema_path=["data"], name="departments"), + referenced_column_paths=[["id"]], + ), + ), + ) + ) + result = [] + for key, definition in definitions: + columns = [path[0] for path in definition.column_paths] + keys = [tuple(row[column] for column in columns) for row in values] + if definition.kind in {ConstraintKind.NOT_NULL, ConstraintKind.PRIMARY_KEY} and any( + None in key for key in keys + ): + raise ValueError("static fixture violates its explicit NOT_NULL definition") + if definition.kind in {ConstraintKind.UNIQUE, ConstraintKind.PRIMARY_KEY}: + present = [key for key in keys if None not in key] + if len(present) != len(set(present)): + raise ValueError("static fixture violates its explicit unique definition") + if definition.kind == ConstraintKind.CHECK and any(row["budget"] < 0 for row in values): + raise ValueError("static fixture violates its fixed budget CHECK") + referenced = None + if definition.kind == ConstraintKind.FOREIGN_KEY: + parents = set(STATIC_CATALOG_ROWS["departments_scan"]["id"].to_pylist()) + if any(row["department_id"] is not None and row["department_id"] not in parents for row in values): + raise ValueError("static fixture violates its department reference") + referenced = CatalogObjectRef( + kind="constraint", schema_path=["data"], owner_table="departments", name="primary-key" + ) + result.append(fixed_fact(name, ["data"], key, definition, referenced_constraint=referenced)) + return tuple(result) + + +def _fixture_columns(name: str, columns: pa.Schema, version: int = 3) -> pa.Schema: + """Publish nullability justified by the already-validated explicit facts.""" + required = { + path[0] + for info in _fixture_constraints(name, version) + if info.definition.kind in {ConstraintKind.NOT_NULL, ConstraintKind.PRIMARY_KEY} + for path in info.definition.column_paths + } + metadata: dict[bytes | str, bytes | str] | None = None + if columns.metadata is not None: + metadata = {key: value for key, value in columns.metadata.items()} + return pa.schema([column.with_nullable(column.name not in required) for column in columns], metadata=metadata) + + _EXAMPLE_CATALOG = Catalog( name="example", default_schema="main", @@ -1208,11 +1326,10 @@ def _build_fixture_stats() -> tuple[ # ----- Constraint example tables ----- Table( name="departments", - columns=schema(id=pa.int64(), name=pa.string(), budget=pa.float64()), - primary_key=(("id",),), - not_null=("id", "name"), - unique=(("name",),), - check=("budget >= 0",), + columns=_fixture_columns( + "departments", schema(id=pa.int64(), name=pa.string(), budget=pa.float64()) + ), + constraints=_fixture_constraints("departments"), defaults={"budget": 0}, statistics={ "id": ColumnStatisticsInput(min=1, max=10, has_null=False, distinct_count=10), @@ -1231,14 +1348,16 @@ def _build_fixture_stats() -> tuple[ ), Table( name="products", - columns=schema( - id=pa.int64(), - name=pa.string(), - quantity=pa.int64(), - price=pa.float64(), + columns=_fixture_columns( + "products", + schema( + id=pa.int64(), + name=pa.string(), + quantity=pa.int64(), + price=pa.float64(), + ), ), - not_null=("id",), - primary_key=(("id",),), + constraints=_fixture_constraints("products"), defaults={ "quantity": 0, "name": "unknown", @@ -1267,40 +1386,29 @@ def _build_fixture_stats() -> tuple[ ), Table( name="employees", - columns=schema( - id=pa.int64(), - name=pa.string(), - email=pa.string(), - department_id=pa.int64(), - ), - primary_key=(("id",),), - not_null=("id", "name", "email"), - unique=(("email",),), - foreign_key=( - ForeignKeyDef( - columns=("department_id",), - referenced_table="departments", - referenced_columns=("id",), + columns=_fixture_columns( + "employees", + schema( + id=pa.int64(), + name=pa.string(), + email=pa.string(), + department_id=pa.int64(), ), ), + constraints=_fixture_constraints("employees"), comment="Employee table with FK to departments", ), Table( name="projects", - columns=schema( - department_id=pa.int64(), - project_code=pa.string(), - title=pa.string(), - ), - primary_key=(("department_id", "project_code"),), - not_null=("department_id", "project_code", "title"), - foreign_key=( - ForeignKeyDef( - columns=("department_id",), - referenced_table="departments", - referenced_columns=("id",), + columns=_fixture_columns( + "projects", + schema( + department_id=pa.int64(), + project_code=pa.string(), + title=pa.string(), ), ), + constraints=_fixture_constraints("projects"), comment="Projects with composite PK and FK to departments", ), # filter_echo_table — catalog table that echoes the pushed-down @@ -1323,31 +1431,31 @@ def _build_fixture_stats() -> tuple[ Table( name="rff_simple", columns=RFF_SIMPLE_COLUMNS, - required_filters=(("a",),), + required_filters=((("a",),),), comment="rff_simple — requires a filter referencing column 'a'.", ), Table( name="rff_struct", columns=RFF_STRUCT_COLUMNS, - required_filters=(("s.a",), ("s.b",)), + required_filters=((("s", "a"),), (("s", "b"),)), comment="rff_struct — requires filters on both struct subfields s.a and s.b.", ), Table( name="rff_nested", columns=RFF_NESTED_COLUMNS, - required_filters=(("wrapper.mid.leaf",),), + required_filters=((("wrapper", "mid", "leaf"),),), comment="rff_nested — requires a filter on the 3-deep nested path wrapper.mid.leaf.", ), Table( name="rff_multi", columns=RFF_MULTI_COLUMNS, - required_filters=(("top",), ("s.a",)), + required_filters=((("top",),), (("s", "a"),)), comment="rff_multi — mixed top-level + struct subfield requirements.", ), Table( name="rff_or", columns=RFF_SIMPLE_COLUMNS, - required_filters=(("a", "b"),), + required_filters=((("a",), ("b",)),), comment="rff_or — requires a filter on 'a' OR 'b' (a single OR-group).", ), Table( @@ -1359,10 +1467,10 @@ def _build_fixture_stats() -> tuple[ name="rff_rowid", columns=RFF_ROWID_COLUMNS, required_filters=( - ("bbox.xmin",), - ("bbox.xmax",), - ("bbox.ymin",), - ("bbox.ymax",), + (("bbox", "xmin"),), + (("bbox", "xmax"),), + (("bbox", "ymin"),), + (("bbox", "ymax"),), ), comment="rff_rowid — row_id virtual column + required bbox.* filters.", ), @@ -1387,10 +1495,10 @@ def _build_fixture_stats() -> tuple[ ] ), required_filters=( - ("bbox.xmin",), - ("bbox.xmax",), - ("bbox.ymin",), - ("bbox.ymax",), + (("bbox", "xmin"),), + (("bbox", "xmax"),), + (("bbox", "ymin"),), + (("bbox", "ymax"),), ), comment="rff_parquet — native read_parquet delegation with bbox.* required filters.", ), @@ -1420,10 +1528,10 @@ def _build_fixture_stats() -> tuple[ ] ), required_filters=( - ("bbox.xmin",), - ("bbox.xmax",), - ("bbox.ymin",), - ("bbox.ymax",), + (("bbox", "xmin"),), + (("bbox", "xmax"),), + (("bbox", "ymin"),), + (("bbox", "ymax"),), ), comment="rff_hive — native read_parquet over Hive glob with bbox.* required filters.", ), @@ -1454,34 +1562,28 @@ def _build_fixture_stats() -> tuple[ ] ), required_filters=( - ("id",), - ("bbox.xmin",), - ("bbox.xmax",), - ("bbox.ymin",), - ("bbox.ymax",), + (("id",),), + (("bbox", "xmin"),), + (("bbox", "xmax"),), + (("bbox", "ymin"),), + (("bbox", "ymax"),), ), comment="rff_hive_mixed — native read_parquet, top-level 'id' + bbox.* required filters.", ), # Time-travel constraint evolution table Table( name="versioned_constraints", - columns=schema( - id=pa.int64(), - name=pa.string(), - email=pa.string(), - department_id=pa.int64(), - ), - supports_time_travel=True, - not_null=("id", "name"), - primary_key=(("id",),), - unique=(("email",),), - foreign_key=( - ForeignKeyDef( - columns=("department_id",), - referenced_table="departments", - referenced_columns=("id",), + columns=_fixture_columns( + "versioned_constraints", + schema( + id=pa.int64(), + name=pa.string(), + email=pa.string(), + department_id=pa.int64(), ), ), + supports_time_travel=True, + constraints=_fixture_constraints("versioned_constraints"), comment="Table with constraints that evolve across versions", ), ], @@ -1495,18 +1597,18 @@ def _build_fixture_stats() -> tuple[ ], indexes=[ Index( - name="idx_numbers_value", - table_name="numbers", - expressions=("value",), - comment="Index on numbers.value", - ), - Index( - name="idx_numbers_value_unique", - table_name="numbers", - expressions=("value",), - constraint_type=IndexConstraintType.UNIQUE, - comment="Unique index on numbers.value", - ), + name=name, + definition=IndexDefinition( + table=CatalogObjectRef(kind="table", schema_path=["data"], name="numbers"), + algorithm="fixture.metadata", + keys=[IndexKey(column_path=["value"]).serialize_to_bytes()], + options=TypedValueBatch.empty(), + constraint_type=IndexConstraintType.NONE, + ), + options=OptionState(values=TypedValueBatch.empty()), + comment="Metadata-only index teaching fixture; no physical index or enforcement", + ) + for name in ("idx_numbers_value", "idx_numbers_value_unique") ], ), ], @@ -1523,6 +1625,10 @@ class ExampleCatalog(ReadOnlyCatalogInterface): catalog = _EXAMPLE_CATALOG + def catalog_capabilities(self, *, attach_opaque_data: AttachOpaqueData) -> CatalogCapabilities: + """Expose validated immutable facts; this catalog has no mutation path.""" + return CatalogCapabilities(read=sorted([CONSTRAINT_RECORDS_READ, ROW_SCALAR_EXPRESSION, INDEX_DEFINITION_READ])) + def table_get( self, *, @@ -1546,9 +1652,7 @@ def table_get( name=name, schema_path=schema_path, columns=SerializedSchema(cols.serialize().to_pybytes()), - not_null_constraints=[], - unique_constraints=[], - check_constraints=[], + constraints=[], comment="Versioned data table demonstrating time travel with schema evolution", tags={}, ) @@ -1559,50 +1663,13 @@ def table_get( ): version = resolve_versioned_constraints_version(at_unit, at_value) cols = _VERSIONED_CONSTRAINTS_SCHEMAS[version] - # Constraints evolve with version: - # V1: NOT NULL on id only - # V2: NOT NULL on id+name, PK on id, UNIQUE on email - # V3: NOT NULL on id+name, PK on id, UNIQUE on email, FK department_id→departments.id - not_null: list[int] = [] - pk: list[list[int]] = [] - unique: list[list[int]] = [] - fk: list[bytes] = [] - col_names = [f.name for f in cols] - if version >= 1: - not_null.append(col_names.index("id")) - if version >= 2: - not_null.append(col_names.index("name")) - pk.append([col_names.index("id")]) - unique.append([col_names.index("email")]) - if version >= 3: - from vgi_rpc.utils import serialize_record_batch_bytes - - fk_batch = pa.RecordBatch.from_pydict( - { - "fk_columns": [["department_id"]], - "pk_columns": [["id"]], - "referenced_table": ["departments"], - "referenced_schema_path": [schema_path], - }, - schema=pa.schema( - [ - ("fk_columns", pa.list_(pa.utf8())), - ("pk_columns", pa.list_(pa.utf8())), - ("referenced_table", pa.utf8()), - ("referenced_schema_path", pa.list_(pa.utf8())), - ] - ), - ) - fk.append(serialize_record_batch_bytes(fk_batch)) + constraints = _fixture_constraints("versioned_constraints", version) + cols = _fixture_columns("versioned_constraints", cols, version) return TableInfo( name=name, schema_path=schema_path, columns=SerializedSchema(cols.serialize().to_pybytes()), - not_null_constraints=not_null, - unique_constraints=unique, - check_constraints=[], - primary_key_constraints=pk, - foreign_key_constraints=fk, + constraints=[fact.serialize_to_bytes() for fact in constraints], comment="Table with constraints that evolve across versions", tags={}, ) @@ -2152,7 +2219,7 @@ class ExampleWorker(Worker): @classmethod def hosted_protocols(cls) -> Sequence[tuple[type, object]]: - """Host ``conformance.Secondary.v1`` beside ``vgi.v2``, on every transport. + """Host ``conformance.Secondary.v1`` beside ``vgi.v3``, on every transport. The cross-SDK hosted-protocols group (``vgi-rpc-test-hosted``) expects every SDK fixture worker to host the vgi-rpc reference secondary. Only diff --git a/vgi/_test_fixtures/writable/generic.py b/vgi/_test_fixtures/writable/generic.py index fc48b544..839a2a56 100644 --- a/vgi/_test_fixtures/writable/generic.py +++ b/vgi/_test_fixtures/writable/generic.py @@ -90,7 +90,7 @@ def on_bind(cls, params: BindParams[None]) -> BindResponse: """Bind: query transactor for table schema (already includes rowid).""" table_name = _get_table_name_from_bind(params) attach_opaque_data = params.attach_opaque_data # unwrapped plaintext - tx_id = params.bind_call.transaction_opaque_data + tx_id = params.transaction_opaque_data assert attach_opaque_data is not None and tx_id is not None table_schema = _get_table_schema_from_transactor(table_name, attach_opaque_data, tx_id) return BindResponse(output_schema=table_schema) @@ -148,7 +148,7 @@ def on_bind(cls, params: BindParams[None]) -> BindResponse: result_mode = _result_mode(params) if result_mode != "count": attach_opaque_data = params.attach_opaque_data # unwrapped plaintext - tx_id = params.bind_call.transaction_opaque_data + tx_id = params.transaction_opaque_data assert attach_opaque_data is not None and tx_id is not None table_schema = _get_table_schema_from_transactor(table_name, attach_opaque_data, tx_id) user_fields = [f for f in table_schema if f.name not in ("rowid", "row_id")] diff --git a/vgi/_test_fixtures/writable/table.py b/vgi/_test_fixtures/writable/table.py index 73e1f138..ead97848 100644 --- a/vgi/_test_fixtures/writable/table.py +++ b/vgi/_test_fixtures/writable/table.py @@ -72,9 +72,8 @@ def _result_mode(params: BindParams[None]) -> WriteResultMode: def _get_tx_id(params: ProcessParams[None]) -> bytes: - """Get transaction_opaque_data from the bind request.""" - assert params.init_call is not None - tx_id = params.init_call.bind_call.transaction_opaque_data + """Get the authenticated provider transaction bytes from callback params.""" + tx_id = params.transaction_opaque_data if tx_id: return tx_id msg = "transaction_opaque_data is required but was not provided in the bind request" diff --git a/vgi/_test_fixtures/writable/worker.py b/vgi/_test_fixtures/writable/worker.py index ec1a53ee..4b0778f0 100644 --- a/vgi/_test_fixtures/writable/worker.py +++ b/vgi/_test_fixtures/writable/worker.py @@ -44,11 +44,18 @@ SchemaInfo, SchemaObjectType, SerializedSchema, + SqlExpression, TableInfo, TransactionOpaqueData, ViewInfo, ) +from vgi.catalog.layout import TableLayout +from vgi.catalog.sql import column_expressions +from vgi.column_path import column_definition as decode_column_definition +from vgi.column_path import single_column_name +from vgi.protocol import ClientCapabilities from vgi.schema_path import sql_qualified_name +from vgi.typed_values import TypedValueBatch from vgi.worker import Worker logger = logging.getLogger("vgi.writable_worker") @@ -206,6 +213,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ctx: CallContext | None = None, ) -> CatalogAttachResult: """Attach: generate unique attach_opaque_data and register a fresh database in the transactor.""" @@ -264,6 +272,19 @@ def catalog_transaction_rollback( # ========== DDL helpers ========== + @staticmethod + def _reject_unqualified_column_expressions(field: pa.Field[Any]) -> None: + """Reject definitions the transactor cannot preserve or evaluate faithfully.""" + expressions = column_expressions(field) + if ( + expressions.default is not None + or expressions.generated is not None + or {b"default", b"generated_expression"}.intersection(field.metadata or {}) + ): + raise NotImplementedError("writable transactor does not preserve canonical column expression contexts") + for index in range(field.type.num_fields): + WritableCatalog._reject_unqualified_column_expressions(field.type.field(index)) + def _execute_ddl( self, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None, sql: str ) -> None: @@ -284,11 +305,11 @@ def table_create( name: str, columns: SerializedSchema, on_conflict: OnConflict, - not_null_constraints: list[int], - unique_constraints: list[list[int]], - check_constraints: list[str], - primary_key_constraints: list[list[int]] | None = None, - foreign_key_constraints: list[bytes] | None = None, + constraint_definitions: list[bytes], + comment: str | None = None, + tags: dict[str, str] | None = None, + options: TypedValueBatch | None = None, + layout: TableLayout | None = None, ) -> None: """Create a new table in the transactor's DuckDB database. @@ -296,6 +317,11 @@ def table_create( DuckDB's built-in rowid pseudocolumn provides the row identifier automatically — no extra column or sequence needed. """ + if constraint_definitions: + raise NotImplementedError("writable fixture has not implemented canonical constraint identities") + if any(value is not None for value in (comment, tags, options, layout)): + raise NotImplementedError("table creation metadata/options/layout profiles are not supported") + # Deserialize columns schema col_schema = pa.ipc.read_schema(pa.BufferReader(columns)) # type: ignore[arg-type] @@ -303,46 +329,14 @@ def table_create( # Build column definitions (including defaults from Arrow field metadata) col_defs: list[str] = [] - for i, field in enumerate(col_schema): + for field in col_schema: + if not field.nullable: + raise NotImplementedError("writable fixture requires canonical NOT_NULL support") col_def = f"{_qi(field.name)} {_arrow_type_to_sql(field.type)}" - if i in not_null_constraints: - col_def += " NOT NULL" - # Default values are passed as Arrow field metadata with key "default" - if field.metadata and b"default" in field.metadata: - default_expr = field.metadata[b"default"].decode("utf-8") - col_def += f" DEFAULT {default_expr}" + self._reject_unqualified_column_expressions(field) col_defs.append(col_def) - # Table-level constraints - constraints: list[str] = [] - - # Primary key - if primary_key_constraints: - for pk_group in primary_key_constraints: - pk_cols = ", ".join(_qi(col_schema.field(i).name) for i in pk_group) - constraints.append(f"PRIMARY KEY ({pk_cols})") - - # Unique constraints - for unique_group in unique_constraints: - uq_cols = ", ".join(_qi(col_schema.field(i).name) for i in unique_group) - constraints.append(f"UNIQUE ({uq_cols})") - - # Check constraints - for check_expr in check_constraints: - constraints.append(f"CHECK ({check_expr})") - - # Foreign key constraints - if foreign_key_constraints: - for fk_bytes in foreign_key_constraints: - fk = _deserialize_fk(fk_bytes) - fk_cols = ", ".join(_qi(c) for c in fk["fk_columns"]) - pk_cols = ", ".join(_qi(c) for c in fk["pk_columns"]) - ref_table = _qi(fk["referenced_table"]) - constraints.append(f"FOREIGN KEY ({fk_cols}) REFERENCES {ref_table}({pk_cols})") - - # Combine into CREATE TABLE - all_parts = col_defs + constraints - columns_sql = ",\n ".join(all_parts) + columns_sql = ",\n ".join(col_defs) ddl = f"CREATE TABLE{if_not_exists} {_qn(schema_path, name)} (\n {columns_sql}\n);" self._execute_ddl(attach_opaque_data, transaction_opaque_data, ddl) logger.info("table_create: %s (on_conflict=%s)", name, on_conflict.value) @@ -390,15 +384,21 @@ def table_column_add( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, + column_path: list[str], column_definition: SerializedSchema, ignore_not_found: bool, if_column_not_exists: bool, ) -> None: """Add a column to a table.""" - col_schema = pa.ipc.read_schema(pa.BufferReader(column_definition)) # type: ignore[arg-type] - field = col_schema.field(0) + column_name = single_column_name(column_path) + field = decode_column_definition(column_definition, [column_name]) + if not field.nullable: + raise NotImplementedError("NOT NULL on ADD COLUMN is not supported; use a separate nullability change") + if any(key in (field.metadata or {}) for key in (b"comment", b"tags", b"compression")): + raise NotImplementedError("ADD COLUMN comments, tags, and compression require a definition profile") if_not_exists = " IF NOT EXISTS" if if_column_not_exists else "" - col_sql = f"{_qi(field.name)} {_arrow_type_to_sql(field.type)}" + col_sql = f"{_qi(column_name)} {_arrow_type_to_sql(field.type)}" + self._reject_unqualified_column_expressions(field) ddl = f"ALTER TABLE {_qn(schema_path, name)} ADD COLUMN{if_not_exists} {col_sql};" self._execute_ddl(attach_opaque_data, transaction_opaque_data, ddl) logger.info("table_column_add: %s.%s", name, field.name) @@ -410,12 +410,13 @@ def table_column_drop( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool, if_column_exists: bool, cascade: bool, ) -> None: """Drop a column from a table.""" + column_name = single_column_name(column_path) if_exists = " IF EXISTS" if if_column_exists else "" cascade_sql = " CASCADE" if cascade else "" ddl = f"ALTER TABLE {_qn(schema_path, name)} DROP COLUMN{if_exists} {_qi(column_name)}{cascade_sql};" @@ -429,11 +430,12 @@ def table_column_rename( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], new_column_name: str, ignore_not_found: bool, ) -> None: """Rename a column in a table.""" + column_name = single_column_name(column_path) ddl = f"ALTER TABLE {_qn(schema_path, name)} RENAME COLUMN {_qi(column_name)} TO {_qi(new_column_name)};" self._execute_ddl(attach_opaque_data, transaction_opaque_data, ddl) logger.info("table_column_rename: %s.%s -> %s", name, column_name, new_column_name) @@ -460,11 +462,12 @@ def table_column_comment_set( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], comment: str | None, ignore_not_found: bool, ) -> None: """Set or clear the comment on a table column.""" + column_name = single_column_name(column_path) target = f"COLUMN {_qn(schema_path, name)}.{_qi(column_name)}" self._execute_ddl(attach_opaque_data, transaction_opaque_data, _comment_sql(target, comment)) @@ -475,18 +478,18 @@ def table_column_type_change( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, + column_path: list[str], column_definition: SerializedSchema, - expression: str | None, + expression: SqlExpression | None, ignore_not_found: bool, ) -> None: """Change the type of a column in a table.""" - col_schema = pa.ipc.read_schema(pa.BufferReader(column_definition)) # type: ignore[arg-type] - field = col_schema.field(0) + column_name = single_column_name(column_path) + field = decode_column_definition(column_definition, [column_name]) col_type = _arrow_type_to_sql(field.type) sql = f"ALTER TABLE {_qn(schema_path, name)} ALTER COLUMN {_qi(field.name)} TYPE {col_type}" - if expression: - # expression comes from DuckDB's binder (serialized AST), not raw user input - sql += f" USING {expression}" + if expression is not None: + raise NotImplementedError("writable transactor has not qualified canonical USING evaluation contexts") self._execute_ddl(attach_opaque_data, transaction_opaque_data, sql + ";") def table_column_default_set( @@ -496,13 +499,12 @@ def table_column_default_set( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, - column_name: str, - expression: str, + column_path: list[str], + expression: SqlExpression, ignore_not_found: bool, ) -> None: """Set the default expression for a column.""" - ddl = f"ALTER TABLE {_qn(schema_path, name)} ALTER COLUMN {_qi(column_name)} SET DEFAULT {expression};" - self._execute_ddl(attach_opaque_data, transaction_opaque_data, ddl) + raise NotImplementedError("writable transactor does not preserve canonical DEFAULT definitions and contexts") def table_column_default_drop( self, @@ -511,10 +513,11 @@ def table_column_default_drop( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool, ) -> None: """Drop the default expression for a column.""" + column_name = single_column_name(column_path) ddl = f"ALTER TABLE {_qn(schema_path, name)} ALTER COLUMN {_qi(column_name)} DROP DEFAULT;" self._execute_ddl(attach_opaque_data, transaction_opaque_data, ddl) @@ -525,12 +528,11 @@ def table_not_null_set( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool, ) -> None: """Set NOT NULL constraint on a column.""" - ddl = f"ALTER TABLE {_qn(schema_path, name)} ALTER COLUMN {_qi(column_name)} SET NOT NULL;" - self._execute_ddl(attach_opaque_data, transaction_opaque_data, ddl) + raise NotImplementedError("writable fixture has not implemented canonical constraint mutation") def table_not_null_drop( self, @@ -539,12 +541,11 @@ def table_not_null_drop( transaction_opaque_data: TransactionOpaqueData | None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool, ) -> None: """Drop NOT NULL constraint from a column.""" - ddl = f"ALTER TABLE {_qn(schema_path, name)} ALTER COLUMN {_qi(column_name)} DROP NOT NULL;" - self._execute_ddl(attach_opaque_data, transaction_opaque_data, ddl) + raise NotImplementedError("writable fixture has not implemented canonical constraint mutation") # ========== Schema discovery (merge static + dynamic) ========== @@ -642,8 +643,13 @@ def schema_create( on_conflict: OnConflict = OnConflict.ERROR, comment: str | None, tags: dict[str, str] | None, + options: TypedValueBatch | None = None, ) -> None: """Create a new schema in the transactor's DuckDB database.""" + if options is not None: + raise NotImplementedError("Schema options are not supported by this fixture") + if comment is not None or tags or on_conflict == OnConflict.REPLACE: + raise NotImplementedError("Schema metadata and replacement are not supported by this fixture") if_not_exists = " IF NOT EXISTS" if on_conflict == OnConflict.IGNORE else "" qualified_path = ".".join(_qi(component) for component in path) self._execute_ddl( @@ -678,8 +684,19 @@ def view_create( name: str, definition: str, on_conflict: OnConflict, + aliases: list[str] | None = None, + security: str = "regular", + comment: str | None = None, + tags: dict[str, str] | None = None, + column_comments: dict[str, str] | None = None, ) -> None: """Create a new view in the transactor's DuckDB database.""" + if comment is not None or tags or column_comments: + raise NotImplementedError("The transactor does not preserve view creation metadata") + if security != "regular": + raise NotImplementedError("view security mode is unsupported by this provider") + if aliases: + raise NotImplementedError("The transactor view reader does not preserve explicit aliases") if_replace = " OR REPLACE" if on_conflict == OnConflict.REPLACE else "" if_not_exists = " IF NOT EXISTS" if on_conflict == OnConflict.IGNORE else "" sql = f"CREATE{if_replace} VIEW{if_not_exists} {_qn(schema_path, name)} AS {definition};" @@ -829,65 +846,18 @@ def table_get( # Build TableInfo from the transactor schema. # The columns schema includes rowid with is_row_id metadata from the transactor. # Constraints are embedded in schema-level metadata as JSON. + for field in table_schema: + self._reject_unqualified_column_expressions(field) serialized = SerializedSchema(table_schema.serialize().to_pybytes()) - # Parse constraints from schema-level metadata - not_null_constraints: list[int] = [] - unique_constraints: list[list[int]] = [] - check_constraints: list[str] = [] - primary_key_constraints: list[list[int]] = [] - foreign_key_constraints: list[bytes] = [] - + # The transactor has no persisted canonical identities yet. Never + # silently turn its existing constraints into an empty readback list. schema_meta = table_schema.metadata or {} if b"vgi.constraints" in schema_meta: import json - from vgi_rpc.utils import serialize_record_batch_bytes - - constraints = json.loads(schema_meta[b"vgi.constraints"].decode("utf-8")) - # Build column name → index map in Arrow schema space (including rowid at index 0). - # Constraint indices must be in Arrow space so the C++ adjust_col lambda - # can correctly shift them to physical space (excluding rowid). - col_index = {f.name: i for i, f in enumerate(table_schema) if f.name != "rowid"} - for c in constraints: - ctype = c["type"] - cols = c.get("columns") or [] - text = c.get("text") or "" - col_indices = [col_index[cn] for cn in cols if cn in col_index] - if ctype == "NOT NULL": - not_null_constraints.extend(col_indices) - elif ctype == "UNIQUE": - unique_constraints.append(col_indices) - elif ctype == "PRIMARY KEY": - primary_key_constraints.append(col_indices) - elif ctype == "CHECK": - # Extract the check expression from constraint_text - # Format: "CHECK(expr)" - if text.startswith("CHECK(") and text.endswith(")"): - check_constraints.append(text[6:-1]) - elif text: - check_constraints.append(text) - elif ctype == "FOREIGN KEY": - ref_table = c.get("referenced_table") or "" - ref_cols = c.get("referenced_columns") or [] - if ref_table and cols and ref_cols: - fk_batch = pa.RecordBatch.from_pydict( - { - "fk_columns": [list(cols)], - "pk_columns": [list(ref_cols)], - "referenced_table": [ref_table], - "referenced_schema_path": [schema_path], - }, - schema=pa.schema( - [ - ("fk_columns", pa.list_(pa.utf8())), - ("pk_columns", pa.list_(pa.utf8())), - ("referenced_table", pa.utf8()), - ("referenced_schema_path", pa.list_(pa.utf8())), - ] - ), - ) - foreign_key_constraints.append(serialize_record_batch_bytes(fk_batch)) + if json.loads(schema_meta[b"vgi.constraints"].decode("utf-8")): + raise NotImplementedError("writable fixture has not implemented canonical constraint discovery") # Also fetch the table comment try: @@ -901,11 +871,7 @@ def table_get( name=name, schema_path=schema_path, columns=serialized, - not_null_constraints=not_null_constraints, - unique_constraints=unique_constraints, - check_constraints=check_constraints, - primary_key_constraints=primary_key_constraints, - foreign_key_constraints=foreign_key_constraints, + constraints=[], write_result_modes={"insert": "changes", "update": "changes", "delete": "changes"}, comment=table_comment, tags={}, diff --git a/vgi/arguments.py b/vgi/arguments.py index def67894..d55904c4 100644 --- a/vgi/arguments.py +++ b/vgi/arguments.py @@ -18,13 +18,15 @@ import typing import warnings from collections.abc import Callable, Mapping, Sequence -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, ClassVar, Final, TypeVar, overload +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, ClassVar, Final, TypeVar, cast, overload import pyarrow as pa from vgi_rpc import ArrowSerializableDataclass from vgi_rpc.errors import Code +from vgi.typed_values import TypedValueBatch + if TYPE_CHECKING: from pyarrow import Scalar @@ -405,11 +407,16 @@ class Arguments: Attributes: positional: Tuple of positional argument values as pa.Scalar. named: Dictionary mapping argument names to pa.Scalar values. + argument_schema: Optional complete schema using ``positional_`` + and ``named_`` fields. Preserves nullability and metadata that + cannot be recovered from scalars alone. Use ``from_batch`` to retain + the schema of an existing one-row argument batch. """ positional: tuple["Scalar[Any] | None", ...] = () named: dict[str, "Scalar[Any]"] | None = None + argument_schema: pa.Schema | None = field(default=None, kw_only=True, repr=False) def get( self, @@ -533,12 +540,24 @@ def schema(self) -> pa.Schema: Creates a schema with one field per argument: "positional_0", "positional_1", etc. for positional args, and "named_" for named args. Field types are taken directly from scalar values to handle Arrow extension types. + An explicit ``argument_schema`` retains field and schema metadata; its + names and types must match the supplied values. Returns: Arrow schema matching the structure returned by `encoded_dict()`. """ args_dict = self.encoded_dict() + if self.argument_schema is not None: + schema = self.argument_schema + if len(schema) != len(args_dict) or set(schema.names) != set(args_dict): + raise ValueError("argument schema must contain exactly the supplied argument fields") + for definition in schema: + scalar = args_dict[definition.name] + actual_type = pa.null() if scalar is None else scalar.type + if not definition.type.equals(actual_type, check_metadata=True): + raise ValueError(f"argument {definition.name!r} does not match its declared type") + return schema fields: list[pa.Field[Any]] = [] for key, scalar in args_dict.items(): if scalar is None: @@ -547,8 +566,42 @@ def schema(self) -> pa.Schema: fields.append(pa.field(key, scalar.type)) return pa.schema(fields) - @staticmethod - def decode(data: pa.StructScalar) -> "Arguments": + @classmethod + def from_batch(cls, batch: pa.RecordBatch) -> "Arguments": + """Read one argument row without losing field or schema metadata. + + Fields use the existing argument wire names: zero-based contiguous + ``positional_`` and ``named_``. Their order is preserved, + even when named fields appear before positional fields. + """ + values = TypedValueBatch(batch) + positional_values: dict[int, Scalar[Any]] = {} + named: dict[str, Scalar[Any]] = {} + for index, key in enumerate(values.schema.names): + value = batch.column(index)[0] + if key.startswith("positional_"): + suffix = key[len("positional_") :] + if not re.fullmatch(r"0|[1-9][0-9]*", suffix): + raise ValueError(f"invalid positional argument field {key!r}") + # Check before int conversion/allocation: a malformed field must + # not request an arbitrarily large positional tuple. + if len(suffix) > len(str(batch.num_columns)) or int(suffix) >= batch.num_columns: + raise ValueError("positional argument indexes must be contiguous from zero") + positional_values[int(suffix)] = value + elif key.startswith("named_") and key[len("named_") :]: + named[key[len("named_") :]] = value + else: + raise ValueError(f"invalid argument field {key!r}") + if set(positional_values) != set(range(len(positional_values))): + raise ValueError("positional argument indexes must be contiguous from zero") + return cls( + positional=tuple(positional_values[index] for index in range(len(positional_values))), + named=named or None, + argument_schema=values.schema, + ) + + @classmethod + def decode(cls, data: pa.StructScalar) -> "Arguments": """Decode [`Arguments`][] from a serialized dictionary. Args: @@ -558,18 +611,31 @@ def decode(data: pa.StructScalar) -> "Arguments": Deserialized `Arguments` instance. """ - positional: list[Scalar[Any] | None] = [] - named: dict[str, Scalar[Any]] = {} - for key, value in data.items(): - if key.startswith("positional_"): - index = int(key[len("positional_") :]) - while len(positional) <= index: - positional.append(None) - positional[index] = value - elif key.startswith("named_"): - name = key[len("named_") :] - named[name] = value - return Arguments(positional=tuple(positional), named=named or None) + if not isinstance(data, pa.StructScalar): + raise ValueError("arguments require a present struct value") + if not data.is_valid: + # DuckDB 1.5 encodes no arguments as a NULL empty struct. It has no + # values to discard; a NULL populated struct would lose arguments. + if len(data.type) == 0: + return cls() + raise ValueError("arguments require a present struct value") + struct_array = pa.repeat(data, 1) + return cls.from_batch(pa.RecordBatch.from_struct_array(struct_array)) + + def to_batch(self) -> pa.RecordBatch: + """Return one flat argument row with its complete Arrow schema.""" + args_dict = self.encoded_dict() + schema = self.schema() + arrays: list[pa.Array[Any]] = [] + for definition in schema: + scalar = args_dict[definition.name] + arrays.append(pa.nulls(1) if scalar is None else pa.repeat(scalar, 1)) # type: ignore[call-overload] + batch = ( + pa.RecordBatch.from_arrays(arrays, schema=schema) + if arrays + else TypedValueBatch.empty(metadata=schema.metadata).batch + ) + return TypedValueBatch(batch).batch def serialize_to_bytes(self) -> bytes: """Serialize [`Arguments`][] to bytes using Arrow IPC format. @@ -584,27 +650,10 @@ def serialize_to_bytes(self) -> bytes: Serialized bytes containing the `Arguments`. """ - args_dict = self.encoded_dict() - fields: list[pa.Field[Any]] = [] - arrays: list[pa.Array[Any]] = [] - for key, scalar in args_dict.items(): - if scalar is None: - fields.append(pa.field(key, pa.null())) - arrays.append(pa.nulls(1)) - else: - fields.append(pa.field(key, scalar.type)) - arrays.append(pa.repeat(scalar, 1)) # type: ignore[call-overload] - if fields: - struct_array: pa.StructArray = pa.StructArray.from_arrays(arrays, fields=fields) - else: - # Empty args: create a length-1 struct array with no fields - struct_type = pa.struct([]) - struct_array = pa.array([{}], type=struct_type) # type: ignore[assignment] + values = self.to_batch() + struct_array = values.to_struct_array() batch = pa.RecordBatch.from_arrays([struct_array], names=["args"]) - sink = pa.BufferOutputStream() - with pa.ipc.new_stream(sink, batch.schema) as writer: - writer.write_batch(batch) - return sink.getvalue().to_pybytes() + return TypedValueBatch(batch.replace_schema_metadata(values.schema.metadata)).serialize_to_bytes() @staticmethod def deserialize_from_bytes(data: bytes, ipc_validation: Any = None) -> "Arguments": @@ -619,9 +668,15 @@ def deserialize_from_bytes(data: bytes, ipc_validation: Any = None) -> "Argument Deserialized `Arguments` instance. """ - reader = pa.ipc.open_stream(data) - batch = reader.read_next_batch() - return Arguments.decode(batch.column("args")[0]) + batch = TypedValueBatch.deserialize_from_bytes(data, ipc_validation).batch + if batch.schema.names != ["args"] or not pa.types.is_struct(batch.schema.field(0).type): + raise ValueError("argument IPC must contain exactly one args struct column") + if not batch.column(0)[0].is_valid and len(batch.schema.field(0).type) != 0: + raise ValueError("arguments require a present struct value") + values = pa.RecordBatch.from_struct_array(cast(pa.StructArray, batch.column(0))).replace_schema_metadata( + batch.schema.metadata + ) + return Arguments.from_batch(values) class ArgumentValidationError(ValueError): diff --git a/vgi/attach_capabilities.py b/vgi/attach_capabilities.py new file mode 100644 index 00000000..7120cc2c --- /dev/null +++ b/vgi/attach_capabilities.py @@ -0,0 +1,104 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Private capability context inside the attachment's authenticated envelope. + +The MetaWorker routing header wraps this context. Below it, storage and catalog +code still receive the original UUID and provider bytes. The agreement travels +with the attachment, including reconnects and HTTP stream rehydration. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from vgi_rpc import ArrowSerializableDataclass + +from vgi.catalog.capabilities import CatalogCapabilities +from vgi.catalog.query import ClientQueryCapabilities, QueryCapabilities +from vgi.typed_values import TypedValueBatch + +_MAGIC = b"VGI\x00CAP\x00" +_QUERY_MAGIC = b"VGI\x00CAPQ\x00" +_MAX_BYTES = 65536 + + +@dataclass(frozen=True, kw_only=True) +class _QueryAttachment(ArrowSerializableDataclass): + catalog: CatalogCapabilities + offer: ClientQueryCapabilities + selected: QueryCapabilities + + +def encode( + plaintext: bytes, + selected: CatalogCapabilities, + *, + query_offer: ClientQueryCapabilities | None = None, + query_selected: QueryCapabilities | None = None, +) -> bytes: + """Wrap UUID/provider bytes with a validated, bounded agreement.""" + selected.validate_selected() + if query_selected is not None: + if query_offer is None or query_selected.select(query_offer, supports_transactions=True) != query_selected: + raise ValueError("query agreement was not selected from the client offer") + payload = _QueryAttachment(catalog=selected, offer=query_offer, selected=query_selected).serialize_to_bytes() + magic = _QUERY_MAGIC + elif selected.names(): + payload = selected.serialize_to_bytes() + magic = _MAGIC + else: + return plaintext + if len(payload) > _MAX_BYTES or len(plaintext) < 16: + raise ValueError("invalid attachment capability context") + return magic + len(payload).to_bytes(4, "big") + payload + plaintext + + +def split(plaintext: bytes) -> tuple[bytes | None, bytes]: + """Return agreement IPC and framework bytes, rejecting malformed contexts. + + A pre-negotiation attachment has no context and selects no new profiles. + Only a missing marker is legacy; a corrupt marked context never falls back. + """ + magic = _QUERY_MAGIC if plaintext.startswith(_QUERY_MAGIC) else _MAGIC + if not plaintext.startswith(magic): + return None, plaintext + at = len(magic) + if len(plaintext) < at + 4: + raise ValueError("truncated attachment capability context") + size = int.from_bytes(plaintext[at : at + 4], "big") + at += 4 + if not 0 < size <= _MAX_BYTES or len(plaintext) < at + size + 16: + raise ValueError("invalid attachment capability context length") + return plaintext[at : at + size], plaintext[at + size :] + + +def agreement(plaintext: bytes) -> CatalogCapabilities: + """Decode the selection from an already authenticated attachment.""" + if plaintext.startswith(_QUERY_MAGIC): + return _query_attachment(plaintext).catalog + payload, _ = split(plaintext) + selected = CatalogCapabilities() if payload is None else CatalogCapabilities.deserialize_from_bytes(payload) + selected.validate_selected() + return selected + + +def _query_attachment(plaintext: bytes) -> _QueryAttachment: + payload, _ = split(plaintext) + if payload is None: + raise ValueError("missing query attachment context") + batch = TypedValueBatch.deserialize_from_bytes(payload).batch + if not batch.schema.equals(_QueryAttachment.ARROW_SCHEMA, check_metadata=False): + raise ValueError("invalid query attachment context schema") + value = _QueryAttachment.deserialize_from_batch(batch) + value.catalog.validate_selected() + if value.selected.select(value.offer, supports_transactions=True) != value.selected: + raise ValueError("invalid selected query agreement") + return value + + +def query_agreement(plaintext: bytes) -> tuple[ClientQueryCapabilities, QueryCapabilities] | None: + """Recover the immutable query offer/selection from authenticated context.""" + if not plaintext.startswith(_QUERY_MAGIC): + return None + value = _query_attachment(plaintext) + return value.offer, value.selected diff --git a/vgi/attach_ticket.py b/vgi/attach_ticket.py index 7334e7c6..6d2df0ea 100644 --- a/vgi/attach_ticket.py +++ b/vgi/attach_ticket.py @@ -667,7 +667,7 @@ def __init__(self, worker: Any, *, max_ttl_seconds: int | None) -> None: """Build the implementation. Args: - worker: The ``Worker`` or ``MetaWorker`` serving ``vgi.v2``. + worker: The ``Worker`` or ``MetaWorker`` serving ``vgi.v3``. max_ttl_seconds: Lifetime ceiling, or ``None`` for no maximum. """ diff --git a/vgi/catalog/__init__.py b/vgi/catalog/__init__.py index 2791f215..18ec3236 100644 --- a/vgi/catalog/__init__.py +++ b/vgi/catalog/__init__.py @@ -42,13 +42,78 @@ deserialize_column_statistics, serialize_column_statistics, ) +from vgi.catalog.column_metadata import ( + ColumnMetadata, + column_metadata, + validate_column_metadata, + with_column_metadata, +) +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, + ConstraintValidation, + ForeignKeyDefinition, + ForeignKeyMatch, + ReferentialAction, +) +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot, ObjectDependency from vgi.catalog.descriptors import Catalog, ForeignKeyDef, Index, Macro, Schema, Sql, Table, View +from vgi.catalog.indexes import IndexDefinition, IndexKey +from vgi.catalog.macros import MacroOverload, MacroParameter +from vgi.catalog.metadata_changes import CatalogMetadataChanges +from vgi.catalog.query import ( + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, + QueryEvaluationContext, + QueryObjectReference, +) from vgi.catalog.secret_type import SecretTypeSpec from vgi.catalog.setting import Setting, SettingSpec +from vgi.catalog.sql import ColumnExpressions, SqlStatement, column_expressions, with_column_expressions from vgi.catalog.storage import CatalogStorage, CatalogStorageSqlite from vgi.write_results import WriteOperation, WriteResultMode, write_changes_batch, write_result_schema __all__ = [ + "ColumnMetadata", + "column_metadata", + "validate_column_metadata", + "with_column_metadata", + "IndexDefinition", + "IndexKey", + "ConstraintChange", + "ConstraintDefinition", + "ConstraintEnforcement", + "ConstraintInfo", + "ConstraintKind", + "ConstraintOperation", + "ConstraintsApplyRequest", + "ConstraintSnapshot", + "ConstraintValidation", + "ForeignKeyDefinition", + "ForeignKeyMatch", + "ReferentialAction", + "ColumnExpressions", + "SqlStatement", + "column_expressions", + "with_column_expressions", + "CatalogQueryPrepareRequest", + "CatalogQueryPrepareResult", + "ClientQueryCapabilities", + "QueryCapabilities", + "QueryEvaluationContext", + "QueryObjectReference", + "CatalogObjectRef", + "DependencySnapshot", + "ObjectDependency", + "CatalogMetadataChanges", # Type aliases "AttachOpaqueData", "TransactionOpaqueData", @@ -71,6 +136,8 @@ "ForeignKeyInfo", "IndexInfo", "MacroInfo", + "MacroOverload", + "MacroParameter", "CatalogContentsResult", "SchemaContentsInfo", "SchemaInfo", diff --git a/vgi/catalog/capabilities.py b/vgi/catalog/capabilities.py new file mode 100644 index 00000000..e9b56b5d --- /dev/null +++ b/vgi/catalog/capabilities.py @@ -0,0 +1,184 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Versioned catalog profiles selected separately for each attachment. + +An offer is not authorization. Selection is the intersection of the client's +offer, the provider's implementation, and profiles understood by this worker. +An absent offer selects nothing. Existing protocol operations retain their +existing behavior; new operations require their explicit profile. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import ClassVar + +from vgi_rpc import ArrowSerializableDataclass + +from vgi.exceptions import CatalogCapabilityError + +CATALOG_DEPENDENCIES_READ = "vgi.catalog.dependencies.read.v1" +INDEX_DEFINITION_READ = "vgi.index.definition.read.v1" +INDEX_DEFINITION_CREATE = "vgi.index.definition.create.v1" +INDEX_DROP = "vgi.index.drop.v1" +CONSTRAINT_RECORDS_READ = "vgi.constraint.records.read.v1" +CONSTRAINT_CREATE = "vgi.constraint.create.v1" +CONSTRAINT_MUTATE = "vgi.constraint.mutate.v1" +CONSTRAINT_ENFORCE = "vgi.constraint.enforce.v1" +SCHEMA_LIFECYCLE = "vgi.schema.lifecycle.v1" +SCHEMA_METADATA_ALTER = "vgi.schema.metadata.alter.v1" +SCHEMA_OPTIONS_READ = "vgi.schema.options.read.v1" +SCHEMA_OPTIONS_CREATE = "vgi.schema.options.create.v1" +SCHEMA_OPTIONS_ALTER = "vgi.schema.options.alter.v1" +ROW_SCALAR_EXPRESSION = "vgi.expression.duckdb.scalar.v1" +TABLE_OPTIONS_READ = "vgi.table.options.read.v1" +TABLE_OPTIONS_CREATE = "vgi.table.options.create.v1" +TABLE_OPTIONS_ALTER = "vgi.table.options.alter.v1" +TABLE_LAYOUT_READ = "vgi.table.layout.read.v1" +TABLE_LAYOUT_CREATE = "vgi.table.layout.create.v1" +TABLE_LAYOUT_ALTER_FUTURE = "vgi.table.layout.alter_future.v1" +TABLE_LAYOUT_ALTER_REWRITE = "vgi.table.layout.alter_rewrite.v1" +TABLE_METADATA_CREATE = "vgi.table.metadata.create.v1" +COLUMN_NESTED_ADD = "vgi.column.nested.add.v1" +COLUMN_NESTED_DROP = "vgi.column.nested.drop.v1" +COLUMN_NESTED_RENAME = "vgi.column.nested.rename.v1" +COLUMN_NESTED_TYPE = "vgi.column.nested.type.v1" +COLUMN_NESTED_DEFAULT = "vgi.column.nested.default.v1" +COLUMN_NESTED_NULLABILITY = "vgi.column.nested.nullability.v1" +COLUMN_NESTED_COMMENT = "vgi.column.nested.comment.v1" +VIEW_BARRIER_READ = "vgi.view.barrier.read.v1" +VIEW_BARRIER_CREATE = "vgi.view.barrier.create.v1" +TRANSACTION_DDL = "vgi.transaction.ddl.v1" +MACRO_OVERLOADS_READ = "vgi.macro.overloads.read.v1" +MACRO_OVERLOADS_CREATE = "vgi.macro.overloads.create.v1" +LOGICAL_TYPE_TUPLE = "vgi.type.tuple.v1" +LOGICAL_TYPE_VARIANT = "vgi.type.variant.parquet.v1" + +_PROFILE = re.compile(r"[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*\.v[1-9][0-9]*\Z") + + +@dataclass(frozen=True, kw_only=True) +class CatalogCapabilities(ArrowSerializableDataclass): + """Catalog read, DDL, write and transaction profiles. + + Lists contain unique, case-sensitive versioned names. Unknown offers are + ignored, never interpreted as equivalent to a known version. Unknown names + in a selected agreement are rejected. Selection is canonical (sorted) and + includes a profile only if its prerequisites were also selected. + """ + + read: list[str] = field(default_factory=list) + ddl: list[str] = field(default_factory=list) + write: list[str] = field(default_factory=list) + transaction: list[str] = field(default_factory=list) + + _known: ClassVar[dict[str, frozenset[str]]] = { + "read": frozenset( + { + CATALOG_DEPENDENCIES_READ, + CONSTRAINT_RECORDS_READ, + INDEX_DEFINITION_READ, + SCHEMA_OPTIONS_READ, + TABLE_OPTIONS_READ, + TABLE_LAYOUT_READ, + ROW_SCALAR_EXPRESSION, + VIEW_BARRIER_READ, + MACRO_OVERLOADS_READ, + LOGICAL_TYPE_TUPLE, + LOGICAL_TYPE_VARIANT, + } + ), + "ddl": frozenset( + { + COLUMN_NESTED_ADD, + COLUMN_NESTED_DROP, + COLUMN_NESTED_RENAME, + COLUMN_NESTED_TYPE, + COLUMN_NESTED_DEFAULT, + COLUMN_NESTED_NULLABILITY, + COLUMN_NESTED_COMMENT, + CONSTRAINT_CREATE, + INDEX_DEFINITION_CREATE, + INDEX_DROP, + CONSTRAINT_MUTATE, + SCHEMA_METADATA_ALTER, + SCHEMA_LIFECYCLE, + SCHEMA_OPTIONS_CREATE, + SCHEMA_OPTIONS_ALTER, + TABLE_OPTIONS_CREATE, + TABLE_OPTIONS_ALTER, + TABLE_LAYOUT_CREATE, + TABLE_LAYOUT_ALTER_FUTURE, + TABLE_LAYOUT_ALTER_REWRITE, + TABLE_METADATA_CREATE, + VIEW_BARRIER_CREATE, + MACRO_OVERLOADS_CREATE, + } + ), + "write": frozenset({CONSTRAINT_ENFORCE}), + "transaction": frozenset({TRANSACTION_DDL}), + } + _requires: ClassVar[dict[str, frozenset[str]]] = { + INDEX_DEFINITION_CREATE: frozenset({INDEX_DEFINITION_READ}), + INDEX_DROP: frozenset({INDEX_DEFINITION_READ}), + CONSTRAINT_CREATE: frozenset({CONSTRAINT_RECORDS_READ, CONSTRAINT_ENFORCE}), + CONSTRAINT_MUTATE: frozenset({CONSTRAINT_RECORDS_READ}), + CONSTRAINT_ENFORCE: frozenset({CONSTRAINT_RECORDS_READ}), + MACRO_OVERLOADS_CREATE: frozenset({MACRO_OVERLOADS_READ}), + VIEW_BARRIER_CREATE: frozenset({VIEW_BARRIER_READ}), + TABLE_OPTIONS_CREATE: frozenset({TABLE_OPTIONS_READ}), + TABLE_OPTIONS_ALTER: frozenset({TABLE_OPTIONS_READ}), + TABLE_LAYOUT_CREATE: frozenset({TABLE_LAYOUT_READ}), + TABLE_LAYOUT_ALTER_FUTURE: frozenset({TABLE_LAYOUT_READ}), + TABLE_LAYOUT_ALTER_REWRITE: frozenset({TABLE_LAYOUT_READ}), + SCHEMA_OPTIONS_CREATE: frozenset({SCHEMA_OPTIONS_READ}), + SCHEMA_OPTIONS_ALTER: frozenset({SCHEMA_OPTIONS_READ}), + } + + def __post_init__(self) -> None: + """Validate names without rejecting a newer peer's unknown offers.""" + for category in self._known: + names = getattr(self, category) + if not isinstance(names, list) or any(not isinstance(name, str) for name in names): + raise TypeError(f"{category} capabilities must be a list of profile names") + if len(names) != len(set(names)) or any(not _PROFILE.fullmatch(name) for name in names): + raise ValueError(f"{category} capabilities must be unique versioned profile names") + for other, known in self._known.items(): + if category != other and set(names) & known: + raise ValueError(f"capability belongs in {other}, not {category}") + + def names(self) -> frozenset[str]: + """Return a validated snapshot of all offered or selected names.""" + self.__post_init__() + return frozenset(name for category in self._known for name in getattr(self, category)) + + def select(self, offer: CatalogCapabilities | None) -> CatalogCapabilities: + """Intersect provider support with an offer and enforce prerequisites.""" + self.__post_init__() + if offer is None: + return CatalogCapabilities() + offer.__post_init__() + selected = { + category: set(getattr(self, category)) & set(getattr(offer, category)) & known + for category, known in self._known.items() + } + # Iterate so future profiles may depend on other dependent profiles. + while True: + names = set().union(*selected.values()) + removed = {name for name in names if not self._requires.get(name, frozenset()) <= names} + if not removed: + break + selected = {category: names - removed for category, names in selected.items()} + return CatalogCapabilities(**{category: sorted(names) for category, names in selected.items()}) + + def validate_selected(self) -> None: + """Reject malformed, unknown or incomplete attachment agreements.""" + if self.select(self) != self: + raise ValueError("selected catalog capabilities contain unknown, unordered or incomplete profiles") + + def require(self, profile: str, *, available: CatalogCapabilities, operation: str) -> None: + """Check the immutable agreement and current support before effects.""" + self.validate_selected() + if profile not in available.select(self).names(): + raise CatalogCapabilityError(operation, profile) diff --git a/vgi/catalog/catalog_interface.py b/vgi/catalog/catalog_interface.py index 1be12c78..276f11b0 100644 --- a/vgi/catalog/catalog_interface.py +++ b/vgi/catalog/catalog_interface.py @@ -33,12 +33,27 @@ from vgi.catalog.secret_type import SecretTypeSpec from vgi.catalog.setting import SettingSpec from vgi.catalog.snapshot import CatalogSnapshotPolicy + from vgi.protocol import ClientCapabilities + from vgi.query_provider import CatalogQueryProvider import pyarrow as pa from vgi_rpc import ArrowSerializableDataclass, ArrowType -from vgi_rpc.utils import deserialize_record_batch, serialize_record_batch_bytes +from vgi_rpc.utils import IpcValidation, deserialize_record_batch, serialize_record_batch_bytes from vgi.arguments import Arguments, SecretLookupEntry +from vgi.catalog.capabilities import ( + CatalogCapabilities, +) +from vgi.catalog.column_metadata import validate_column_metadata +from vgi.catalog.constraints import ConstraintInfo, ConstraintsApplyRequest, ConstraintSnapshot, constraint_infos +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot +from vgi.catalog.indexes import IndexConstraintType, IndexDefinition +from vgi.catalog.layout import TableLayout, TableLayoutChange +from vgi.catalog.macros import MAX_MACRO_BYTES, MacroOverload, validate_macro_family +from vgi.catalog.metadata_changes import CatalogMetadataChanges +from vgi.catalog.options import OptionChanges, OptionDefinitions, OptionState +from vgi.catalog.query import QueryCapabilities +from vgi.catalog.sql import SqlExpression as SqlExpression from vgi.exceptions import CatalogReadOnlyError, InvalidArgumentError, NotFoundError, UnsupportedOperationError from vgi.metadata import ( ArgumentMonotonicity, @@ -52,7 +67,8 @@ PartitionKind, RuntimeFilterAlgorithmCapability, ) -from vgi.schema_path import SchemaKey, SchemaPath, schema_path_display, schema_path_key +from vgi.schema_path import SchemaKey, SchemaPath, schema_path_display, schema_path_key, validate_default_schema +from vgi.typed_values import TypedValueBatch _FILTER_IDENTITY_LIST_TYPE = pa.list_( pa.struct( @@ -125,7 +141,6 @@ class CatalogExample(ArrowSerializableDataclass): AttachOpaqueData = NewType("AttachOpaqueData", bytes) TransactionOpaqueData = NewType("TransactionOpaqueData", bytes) SerializedSchema = NewType("SerializedSchema", bytes) -SqlExpression = NewType("SqlExpression", str) @dataclass(frozen=True) @@ -270,7 +285,7 @@ class CatalogAttachResult(ArrowSerializableDataclass): persisted across commands. True: Catalog is stateful; attach_opaque_data represents a session. False: Catalog is stateless; CLI can auto-attach on each command. - default_schema: The name of the default schema for this catalog. + default_schema: Raw root schema name, or None when there is no implicit default. settings: Extension options (settings) exposed by this catalog/worker. Each ExtensionOption is serialized as bytes for Arrow compatibility. secret_types: Secret types registered with DuckDB's SecretManager. Each @@ -309,6 +324,9 @@ class CatalogAttachResult(ArrowSerializableDataclass): catalog with one ``catalog_contents`` call instead of ``catalog_schemas`` plus a ``catalog_schema_contents_*`` call per schema and kind. Older workers omit it and read as ``False``. + catalog_capabilities: Current provider profile advertisement, not authorization. + selected_capabilities: Profiles selected for this attachment; absent means none. + query_pushdown: Immutable query agreement selected from the client's offer, or None. """ attach_opaque_data: AttachOpaqueData @@ -317,7 +335,7 @@ class CatalogAttachResult(ArrowSerializableDataclass): catalog_version_frozen: bool catalog_version: int attach_opaque_data_required: bool = True - default_schema: str = "main" + default_schema: str | None = "main" settings: list[bytes] = field(default_factory=list) secret_types: list[bytes] = field(default_factory=list) attach_catalogs: list[bytes] = field(default_factory=list) @@ -329,6 +347,17 @@ class CatalogAttachResult(ArrowSerializableDataclass): resolved_data_version: str | None = field(kw_only=True) resolved_implementation_version: str | None = field(kw_only=True) supports_catalog_contents: bool = field(default=False, kw_only=True) + catalog_capabilities: Annotated[CatalogCapabilities | None, ArrowType(pa.binary())] = field( + default=None, kw_only=True + ) + selected_capabilities: Annotated[CatalogCapabilities | None, ArrowType(pa.binary())] = field( + default=None, kw_only=True + ) + query_pushdown: QueryCapabilities | None = field(default=None, kw_only=True) + + def __post_init__(self) -> None: + """Reject invalid defaults before an attachment can be published.""" + validate_default_schema(self.default_schema) @dataclass(frozen=True) @@ -389,21 +418,22 @@ class SchemaInfo(CatalogObject, ArrowSerializableDataclass): AT-clause queries do not honor the bypass — they always issue the per-name RPC because a historical version may have had entries the current view does not. + options: Effective typed options and explicit overrides when negotiated. """ path: SchemaPath # Constructor-only compatibility for existing catalog authors; never metadata. attach_opaque_data: InitVar[AttachOpaqueData | None] = field(default=None, kw_only=True) estimated_object_count: dict[str, int] | None = None + options: Annotated[OptionState | None, ArrowType(pa.binary())] = None @dataclass(frozen=True, slots=True, kw_only=True) class ForeignKeyInfo(ArrowSerializableDataclass): - """Serialized foreign-key constraint carried inside `TableInfo`. + """Legacy foreign-key convenience record retained for source migration. - `TableInfo.foreign_key_constraints` contains IPC-serialized instances of - this record. It is distinct from the author-facing `ForeignKeyDef`, whose - column-name fields are resolved into this wire representation. + Canonical table carriers use `ForeignKeyDefinition` inside `ConstraintInfo` + or `ConstraintDefinition`. This older record is no longer table wire state. Attributes: fk_columns: Referencing column names on the table carrying the constraint. @@ -425,12 +455,9 @@ class TableInfo(CatalogSchemaObject, ArrowSerializableDataclass): Attributes: columns: The columns of the table as a PyArrow schema that is serialized as bytes. - not_null_constraints: Column indices with a NOT NULL constraint. Uses - ArrowType to specify int32 instead of the default int64. - unique_constraints: Column-index groups with a UNIQUE constraint. - check_constraints: SQL CHECK constraint expressions. - primary_key_constraints: Column-index groups forming the primary key. - foreign_key_constraints: Serialized foreign-key constraint specs. + constraints: Required canonical ConstraintInfo IPC list; empty means none. + definition_revision: Opaque semantic revision, required when canonical + mutation support is selected and never reused after recreation. write_result_modes: Maximum result mode supported for each writable operation. Keys are ``insert``, ``update``, and ``delete``; a missing key means the operation is unsupported. Values are ordered @@ -484,30 +511,22 @@ class TableInfo(CatalogSchemaObject, ArrowSerializableDataclass): path; workers can still inline manually inside their own ``schema_contents`` override when the bind output is independently known to be stable. - required_filters: Required WHERE-filter groups the VGI extension's - optimizer pass verifies against any scan (conjunctive normal form): - the outer list is an AND of groups, each inner group is an OR of - dotted-path column references (top-level names like ``"country"`` or - struct subfields like ``"bbox.xmin"``). A group is satisfied when any - one of its paths has a filter; every group must be satisfied. So - ``[["accession_number"], ["ticker", "cik"]]`` means "accession_number - AND one of (ticker, cik)". Empty (default) means no enforcement — the - zero-cost fast path. Satisfaction is prefix-based: a present filter on - a shorter dotted path satisfies any required path it's a prefix of (a - whole-struct filter on ``bbox`` satisfies every ``"bbox.*"`` path). - The C++ extension throws ``BinderException`` listing any unsatisfied - groups. + required_filters: AND-of-OR groups of raw column paths. Each path is + a nonempty list of exact field names, e.g. ``[[["bbox", "xmin"]], + [["ticker"], ["cik"]]]``. Dots and quotes are literal characters. + A filter on a component prefix satisfies a path; a filter on + ``["bbox"]`` satisfies ``["bbox", "xmin"]``, while ``["bbox.xmin"]`` + is a different identity. Empty means no enforcement. These are + column-presence requirements, not proof of remote enforcement. + options: Negotiated typed option readback, or None when unsupported. + layout: Negotiated partition/sort policy, or None when unsupported. + This metadata never guarantees scan output order. """ columns: SerializedSchema - not_null_constraints: Annotated[list[int], ArrowType(pa.list_(pa.int32()))] - unique_constraints: Annotated[list[list[int]], ArrowType(pa.list_(pa.list_(pa.int32())))] - check_constraints: list[str] - primary_key_constraints: Annotated[list[list[int]], ArrowType(pa.list_(pa.list_(pa.int32())))] = field( - default_factory=list - ) - foreign_key_constraints: Annotated[list[bytes], ArrowType(pa.list_(pa.binary()))] = field(default_factory=list) + constraints: list[bytes] + definition_revision: bytes | None = None write_result_modes: Annotated[dict[str, str], ArrowType(pa.map_(pa.string(), pa.string()))] = field( default_factory=dict @@ -527,10 +546,31 @@ class TableInfo(CatalogSchemaObject, ArrowSerializableDataclass): bind_result: Annotated[bytes | None, ArrowType(pa.binary())] = None - required_filters: Annotated[list[list[str]], ArrowType(pa.list_(pa.list_(pa.string())))] = field( + required_filters: Annotated[list[list[list[str]]], ArrowType(pa.list_(pa.list_(pa.list_(pa.string()))))] = field( default_factory=list ) + options: Annotated[OptionState | None, ArrowType(pa.binary())] = None + layout: Annotated[TableLayout | None, ArrowType(pa.binary())] = None + + def __post_init__(self) -> None: + """Validate canonical identities and revision without inferring enforcement.""" + self.constraint_infos() + validate_column_metadata(pa.ipc.read_schema(pa.py_buffer(self.columns))) + + def serialize_to_bytes(self) -> bytes: + """Validate field metadata again before publishing a table descriptor.""" + self.__post_init__() + return super().serialize_to_bytes() + + def constraint_infos(self) -> list[ConstraintInfo]: + """Decode the complete canonical constraint readback for this table.""" + return constraint_infos( + self.constraints, + CatalogObjectRef(kind="table", schema_path=self.schema_path, name=self.name), + self.definition_revision, + ) + @dataclass(frozen=True) class ViewInfo(CatalogSchemaObject, ArrowSerializableDataclass): @@ -546,40 +586,101 @@ class ViewInfo(CatalogSchemaObject, ArrowSerializableDataclass): aligns these by name against the bound output columns and feeds them into ``CreateViewInfo.column_comments_map``; names that don't match a bound column are ignored. + aliases: Ordered raw output identifiers. A partial list renames the leading + outputs; an empty list retains names from the SELECT. Literal dots and + quotes are part of the identifier, never SQL syntax. + security: ``regular`` or ``barrier``. A barrier prevents unsafe query + optimization across the view; it does not grant privileges. Readers + must enforce the barrier when materializing or reject the view. """ definition: str column_comments: dict[str, str] = field(default_factory=dict) + # Ordered raw output identifiers, never SQL fragments or dotted paths. + # A partial list renames only the leading outputs; [] uses query names. + aliases: list[str] = field(default_factory=list) + security: str = "regular" + @classmethod + def deserialize_from_batch( + cls, + batch: pa.RecordBatch, + custom_metadata: pa.KeyValueMetadata | None = None, + *, + ipc_validation: IpcValidation = IpcValidation.FULL, + ) -> Self: + """Wire metadata must explicitly carry security, even for ordinary views.""" + index = batch.schema.get_field_index("security") + if ( + index < 0 + or batch.num_rows != 1 + or batch.column(index).type != pa.string() + or batch.column(index).null_count + ): + raise ValueError("ViewInfo.security must be an explicit non-null string") + return super().deserialize_from_batch(batch, custom_metadata, ipc_validation=ipc_validation) + + @staticmethod + def validate_aliases(aliases: list[str]) -> None: + """Reject malformed identifiers without changing order or spelling.""" + if not isinstance(aliases, list) or any( + not isinstance(alias, str) or not alias or "\0" in alias for alias in aliases + ): + raise ValueError("view aliases must be a list of nonempty raw identifiers without NUL") + + @staticmethod + def validate_create_metadata( + comment: str | None, + tags: dict[str, str] | None, + column_comments: dict[str, str] | None, + ) -> None: + """Validate creation metadata before a provider can change catalog state.""" + if comment is not None and not isinstance(comment, str): + raise ValueError("view comment must be a string or None") + for label, values in (("tags", tags), ("column_comments", column_comments)): + if values is None: + continue + if not isinstance(values, dict) or any( + not isinstance(k, str) or not isinstance(v, str) for k, v in values.items() + ): + raise ValueError(f"view {label} must map strings to strings") + if column_comments is not None: + ViewInfo.validate_aliases(list(column_comments)) + + @staticmethod + def validate_security(security: str) -> None: + """Reject unknown security modes; never downgrade a barrier.""" + if security not in ("regular", "barrier"): + raise ValueError("view security must be regular or barrier") -@dataclass(frozen=True) -class MacroInfo(CatalogSchemaObject, ArrowSerializableDataclass): - """Information about a macro in a schema. + def __post_init__(self) -> None: + """Validate aliases at the metadata boundary.""" + self.validate_aliases(self.aliases) + self.validate_security(self.security) - Attributes: - macro_type: Whether this is a scalar or table macro. - parameters: Ordered list of parameter names. - parameter_default_values: One-row `RecordBatch` where column names are parameter - names and values are typed defaults. None if no defaults. - Serialized as IPC bytes over the wire. - definition: The SQL expression (scalar) or query (table). - arguments_schema: Optional Arrow schema (serialized as IPC bytes) with one - nullable field per parameter, in ``parameters`` order. Each field's type - is the parameter's default value type when known (else null), and the - ``vgi_doc`` field metadata key carries the parameter's description (UTF-8, - presence-only — omitted when undocumented). Mirrors the per-argument doc - channel functions expose via ``FunctionInfo.arguments``. None means the - worker did not supply per-parameter docs (older workers); the extension - falls back to ``parameters`` for names. Built with - ``vgi.argument_spec.macro_arguments_schema``. - """ +@dataclass(frozen=True) +class MacroInfo(CatalogSchemaObject, ArrowSerializableDataclass): + """One complete macro family with resolved literal defaults on discovery.""" macro_type: "MacroType" - parameters: list[str] - parameter_default_values: Annotated[pa.RecordBatch | None, ArrowType(pa.binary())] = None - definition: str = "" - arguments_schema: Annotated[pa.Schema | None, ArrowType(pa.binary())] = None + overloads: list[MacroOverload] + + def __post_init__(self) -> None: + """Validate the mandatory complete family and reject unresolved defaults.""" + validate_macro_family(macro_type=self.macro_type.value, schema_path=self.schema_path, overloads=self.overloads) + if any( + parameter.default_expression is not None for overload in self.overloads for parameter in overload.parameters + ): + raise ValueError("macro discovery must return defaults resolved at creation") + + def serialize_to_bytes(self) -> bytes: + """Revalidate mutable overload families before catalog publication.""" + self.__post_init__() + result = super().serialize_to_bytes() + if len(result) > MAX_MACRO_BYTES: + raise ValueError("macro record is too large") + return result class FunctionType(Enum): @@ -610,40 +711,24 @@ class MacroType(Enum): TABLE = "table" -class IndexConstraintType(Enum): - """The constraint type of an index. - - Attributes: - NONE: Regular index (no constraint enforcement). - UNIQUE: Index enforces a UNIQUE constraint. - PRIMARY: Index enforces a PRIMARY KEY constraint. - """ - - NONE = "none" - UNIQUE = "unique" - PRIMARY = "primary" - - @dataclass(frozen=True) class IndexInfo(CatalogSchemaObject, ArrowSerializableDataclass): - """Information about an index in a schema. + """Resolved typed index definition and effective options with explicit names.""" - Attributes: - table_name: The name of the table this index is on. - index_type: The index type string (e.g., "ART", or empty for default). - constraint_type: The constraint enforcement type (NONE, UNIQUE, PRIMARY). - expressions: SQL expression strings defining the indexed expressions. - For column-based indexes, these are column references (e.g., "col_a"). - For expression indexes, these are arbitrary SQL (e.g., "lower(col_a)"). - options: Key-value index options (WITH clause). + definition: Annotated[IndexDefinition, ArrowType(pa.binary())] + options: Annotated[OptionState, ArrowType(pa.binary())] - """ + def __post_init__(self) -> None: + """Require complete definitions and consistent effective option values.""" + self.definition.__post_init__() + self.options.__post_init__() + if not self.definition.options.batch.equals(self.options.values.batch, check_metadata=True): + raise ValueError("index definition and readback options disagree") - table_name: str - index_type: str = "" - constraint_type: IndexConstraintType = IndexConstraintType.NONE - expressions: list[str] = field(default_factory=list) - options: dict[str, str] = field(default_factory=dict) + def serialize_to_bytes(self) -> bytes: + """Revalidate nested mutable collections before publication.""" + self.__post_init__() + return super().serialize_to_bytes() class SchemaObjectType(Enum): @@ -1765,6 +1850,9 @@ class CatalogInterface(ABC): #: ``False``. catalog_contents_attach_independent: ClassVar[bool] = False + #: Optional provider for prepared read-only catalog queries. The Worker + #: negotiates and binds it to each attachment; default catalogs select none. + query_provider: "CatalogQueryProvider | None" = None #: Optional framework publication policy; ordinary delivery when unset. catalog_snapshot_policy: ClassVar["CatalogSnapshotPolicy | None"] = None @@ -1864,6 +1952,14 @@ def catalog_transaction_rollback( """ raise UnsupportedOperationError("Catalog transactions not implemented.") + def catalog_capabilities(self, *, attach_opaque_data: AttachOpaqueData) -> CatalogCapabilities: + """Return currently implemented profiles for this attachment, not authorization. + + Re-evaluated before new operations. Do not infer support merely from + a method existing or a codec understanding its records. + """ + return CatalogCapabilities() + @abstractmethod def catalog_attach( self, @@ -1872,6 +1968,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: "ClientCapabilities | None" = None, ctx: "CallContext | None" = None, ) -> CatalogAttachResult: """Attach to a catalog with the given name and options. @@ -1884,6 +1981,15 @@ def catalog_attach( human-readable message; the error surfaces on the client as the ATTACH failure. + ``client_capabilities`` describes the consuming engine's offered + capabilities. ``None`` means unspecified support, not full support. + Implementations may tailor their response and catalog metadata to the + offer. Retain any selected behavior in attachment-scoped state (or the + returned opaque token) so subsequent calls on other worker processes + honor the same agreement. Do not store it on the shared catalog + instance or reuse attachment-independent metadata caches for content + that varies with this agreement. Client offers are not authorization. + ``ctx`` is injected by the RPC dispatcher when available. Over HTTP it enables setting a per-session routing cookie via ``ctx.set_cookie()``; over subprocess it may be ``None`` or have empty cookie support. @@ -1948,10 +2054,66 @@ def schema_create( on_conflict: OnConflict = OnConflict.ERROR, comment: str | None, tags: dict[str, str], + options: TypedValueBatch | None = None, ) -> None: """Create a new schema with the given path, comment, and tags.""" raise UnsupportedOperationError("Schema create not implemented.") + def schema_option_definitions( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + ) -> OptionDefinitions: + """Describe literal options for an existing or proposed schema path. + + Defaults come from this transaction snapshot. Implementations validate + again within each mutation; discovery does not reserve defaults. + """ + raise UnsupportedOperationError("Schema option discovery not implemented.") + + def schema_alter_metadata( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + changes: CatalogMetadataChanges, + ignore_not_found: bool = False, + ) -> None: + """Apply a complete metadata patch atomically, preserving options and contents. + + Validate the patch even when ignoring a missing target. Missing targets + without IGNORE raise; an empty patch is not a schema-existence bypass. + Effective no-ops preserve versions and etags. Metadata changes advance + the transaction-visible version and invalidate committed snapshots on + commit, under the provider's declared transaction guarantees. + """ + raise UnsupportedOperationError("Schema metadata alteration not implemented.") + + def schema_alter_options( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + path: SchemaPath, + changes: OptionChanges, + ) -> None: + """Validate and apply the entire SET/RESET operation atomically.""" + raise UnsupportedOperationError("Schema option alteration not implemented.") + + def catalog_dependencies( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None + ) -> DependencySnapshot: + """Return a complete authoritative graph from one transaction snapshot. + + Do not omit inaccessible dependencies to make the graph appear complete. + Reject this read when policy prevents disclosure; mutations still enforce + all dependencies. Discovery does not reserve or authorize later changes. + """ + raise UnsupportedOperationError("Catalog dependency discovery not implemented.") + def schema_drop( self, *, @@ -2154,27 +2316,60 @@ def table_create( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - # The contents of the table is a serialized PyArrow schema - # the nullability for each field is ignored. - # schema.serialize().to_pybytes() + # Nullability must agree with the canonical enforced constraints. columns: SerializedSchema, on_conflict: OnConflict, - # These are constraints listed by field index - not_null_constraints: list[int], # [] = no not null constraints - unique_constraints: list[list[int]], # [] = no unique constraints - # These are general check constraints specified as SQL expressions. - check_constraints: list[str], # [] = no check constraints - # Primary key constraints as column index groups - primary_key_constraints: list[list[int]] | None = None, - # Foreign key constraints as IPC-serialized bytes (same format as TableInfo) - foreign_key_constraints: list[bytes] | None = None, + constraint_definitions: list[bytes], + comment: str | None = None, + tags: dict[str, str] | None = None, + options: TypedValueBatch | None = None, + layout: TableLayout | None = None, ) -> None: """Create a new table with the given name and schema. - Comments and tags are not supported on table creation. + New metadata, typed options and layout require their selected profiles. """ raise UnsupportedOperationError("Table create not implemented.") + def table_constraints_apply(self, request: ConstraintsApplyRequest) -> ConstraintSnapshot: + """Apply all canonical changes atomically; opaque attachment/tx are unwrapped.""" + raise UnsupportedOperationError("Canonical constraint mutation is not implemented") + + def table_option_definitions( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: SchemaPath, + name: str, + ) -> OptionDefinitions: + """Return typed definitions for a prospective or existing table.""" + raise UnsupportedOperationError("Table option definitions not implemented") + + def table_alter_options( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: SchemaPath, + name: str, + changes: OptionChanges, + ) -> None: + """Validate and persist the whole option change atomically.""" + raise UnsupportedOperationError("Table option alteration not implemented") + + def table_alter_layout( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: SchemaPath, + name: str, + changes: TableLayoutChange, + ) -> None: + """Apply the requested future-write policy or atomic existing-data rewrite.""" + raise UnsupportedOperationError("Table layout alteration not implemented") + def table_drop( self, *, @@ -2208,7 +2403,7 @@ def table_column_comment_set( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], comment: str | None, ignore_not_found: bool, ) -> None: @@ -2237,6 +2432,7 @@ def table_column_add( name: str, # Arrow schema with single field for column to add. # Serialized via schema.serialize().to_pybytes() + column_path: list[str], column_definition: SerializedSchema, ignore_not_found: bool, if_column_not_exists: bool, @@ -2251,7 +2447,7 @@ def table_column_drop( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool, if_column_exists: bool, cascade: bool, @@ -2266,7 +2462,7 @@ def table_column_rename( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], new_column_name: str, ignore_not_found: bool, ) -> None: @@ -2280,7 +2476,7 @@ def table_column_default_set( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], expression: SqlExpression, ignore_not_found: bool, ) -> None: @@ -2294,7 +2490,7 @@ def table_column_default_drop( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool, ) -> None: """Drop the default expression for the column.""" @@ -2309,13 +2505,14 @@ def table_column_type_change( name: str, # Arrow schema with single field for the new column type. # Serialized via schema.serialize().to_pybytes() + column_path: list[str], column_definition: SerializedSchema, expression: SqlExpression | None, ignore_not_found: bool, ) -> None: """Change the type of the column in the table with the given name. - The name of the column to change is taken from the field in the provided schema. + The explicit path selects the field; the definition name must match its final component. """ raise UnsupportedOperationError("Table column type change not implemented.") @@ -2326,7 +2523,7 @@ def table_not_null_drop( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool, ) -> None: """Drop the NOT NULL constraint from the column.""" @@ -2339,7 +2536,7 @@ def table_not_null_set( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool, ) -> None: """Set the NOT NULL constraint on the column.""" @@ -2523,6 +2720,11 @@ def view_create( name: str, definition: str, on_conflict: OnConflict, + aliases: list[str] | None = None, + security: str = "regular", + comment: str | None = None, + tags: dict[str, str] | None = None, + column_comments: dict[str, str] | None = None, ) -> None: """Create a new view with the given definition.""" raise UnsupportedOperationError("View create not implemented.") @@ -2580,6 +2782,20 @@ def view_comment_set( """Set the comment for the view with the given name.""" raise UnsupportedOperationError("View comment set not implemented.") + def view_column_comment_set( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + schema_path: SchemaPath, + name: str, + column_path: list[str], + comment: str | None, + ignore_not_found: bool, + ) -> None: + """Set or clear the comment keyed by a raw view output name.""" + raise UnsupportedOperationError("View column comment set not implemented.") + # ---- Macros ---- @abstractmethod @@ -2604,28 +2820,13 @@ def macro_create( schema_path: SchemaPath, name: str, macro_type: "MacroType", - parameters: list[str], - definition: str, + overloads: list[MacroOverload], on_conflict: OnConflict, - parameter_default_values: pa.RecordBatch | None = None, - arguments_schema: pa.Schema | None = None, ) -> None: - """Create a new macro with the given definition. + """Bind and atomically store a complete macro family. - Args: - attach_opaque_data: Per-attach catalog session token. - transaction_opaque_data: Optional transaction handle. - schema_path: Schema to create the macro in. - name: Name for the new macro. - macro_type: Whether this is a scalar or table macro. - parameters: Ordered list of parameter names. - definition: SQL expression (scalar) or query (table). - on_conflict: Behavior if the macro already exists. - parameter_default_values: One-row ``RecordBatch`` with typed defaults. - arguments_schema: Optional Arrow schema (one nullable field per - parameter, in ``parameters`` order) carrying per-parameter - descriptions via the ``vgi_doc`` field metadata key. ``None`` when - no per-parameter docs are supplied. + Providers resolve supported expression defaults once at creation and + return resolved literal defaults on discovery. """ raise UnsupportedOperationError("Macro create not implemented.") @@ -2658,6 +2859,17 @@ def index_get( """ return None + def index_option_definitions( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None, + table: CatalogObjectRef, + algorithm: str, + ) -> OptionDefinitions: + """Return supported typed options for the exact table and algorithm.""" + raise UnsupportedOperationError("Index option definitions not implemented.") + def index_create( self, *, @@ -2665,14 +2877,10 @@ def index_create( transaction_opaque_data: TransactionOpaqueData | None, schema_path: SchemaPath, name: str, - table_name: str, - index_type: str, - constraint_type: IndexConstraintType, - expressions: list[str], + definition: IndexDefinition, on_conflict: OnConflict, - options: dict[str, str] | None = None, ) -> None: - """Create a new index on the specified table.""" + """Bind and atomically construct a physical index before publishing it.""" raise UnsupportedOperationError("Index create not implemented.") def index_drop( @@ -2805,8 +3013,10 @@ class ReadOnlyCatalogInterface(CatalogInterface): catalog_transaction_commit: DDL stub that raises `[`CatalogReadOnlyError`][]. catalog_transaction_rollback: DDL stub that raises `[`CatalogReadOnlyError`][]. schema_create: DDL stub that raises `[`CatalogReadOnlyError`][]. + schema_alter_metadata: DDL stub that raises `[`CatalogReadOnlyError`][]. schema_drop: DDL stub that raises `[`CatalogReadOnlyError`][]. table_create: DDL stub that raises `[`CatalogReadOnlyError`][]. + table_constraints_apply: DDL stub that raises `[`CatalogReadOnlyError`][]. table_drop: DDL stub that raises `[`CatalogReadOnlyError`][]. table_comment_set: DDL stub that raises `[`CatalogReadOnlyError`][]. table_column_comment_set: DDL stub that raises `[`CatalogReadOnlyError`][]. @@ -2822,6 +3032,7 @@ class ReadOnlyCatalogInterface(CatalogInterface): view_create: DDL stub that raises `[`CatalogReadOnlyError`][]. view_drop: DDL stub that raises `[`CatalogReadOnlyError`][]. view_rename: DDL stub that raises `[`CatalogReadOnlyError`][]. + view_column_comment_set: DDL stub that raises `[`CatalogReadOnlyError`][]. view_comment_set: DDL stub that raises `[`CatalogReadOnlyError`][]. macro_create: DDL stub that raises `[`CatalogReadOnlyError`][]. macro_drop: DDL stub that raises `[`CatalogReadOnlyError`][]. @@ -2992,7 +3203,7 @@ def _effective_catalog_name(self) -> str: return self.catalog_name @property - def _default_schema_name(self) -> str: + def _default_schema_name(self) -> str | None: """Get default schema name.""" if self.catalog is not None: return self.catalog.default_schema @@ -3021,6 +3232,7 @@ def catalog_attach( options: dict[str, Any], data_version_spec: str | None, implementation_version: str | None, + client_capabilities: "ClientCapabilities | None" = None, ctx: "CallContext | None" = None, ) -> CatalogAttachResult: """Attach to the catalog. Version constraints are ignored by default.""" @@ -3268,7 +3480,7 @@ def table_scan_function_get( ] available_str = ", ".join(sorted(available)) if available else "(none)" - raise NotImplementedError( + raise UnsupportedOperationError( f"table_scan_function_get not implemented for table " f"{self._effective_catalog_name}.{schema_path_display(schema_path)}::{name!r}. " f"Available tables: {available_str}. " @@ -3720,8 +3932,10 @@ def _function_to_info(self, func_cls: type, schema_path: SchemaPath) -> Function catalog_transaction_commit = _read_only("commit transaction") catalog_transaction_rollback = _read_only("rollback transaction") schema_create = _read_only("create schema") + schema_alter_metadata = _read_only("alter schema metadata") schema_drop = _read_only("drop schema") table_create = _read_only("create table") + table_constraints_apply = _read_only("mutate constraints") table_drop = _read_only("drop table") table_comment_set = _read_only("set table comment") table_column_comment_set = _read_only("set column comment") @@ -3737,6 +3951,7 @@ def _function_to_info(self, func_cls: type, schema_path: SchemaPath) -> Function view_create = _read_only("create view") view_drop = _read_only("drop view") view_rename = _read_only("rename view") + view_column_comment_set = _read_only("set view column comment") view_comment_set = _read_only("set view comment") macro_create = _read_only("create macro") macro_drop = _read_only("drop macro") diff --git a/vgi/catalog/column_metadata.py b/vgi/catalog/column_metadata.py new file mode 100644 index 00000000..e19651b0 --- /dev/null +++ b/vgi/catalog/column_metadata.py @@ -0,0 +1,109 @@ +# Copyright 2026 Query Farm LLC - https://query.farm + +"""Strict comment and tag metadata on existing Arrow column definitions.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Any, cast + +import pyarrow as pa + +COMMENT_KEY = b"comment" +TAGS_KEY = b"tags" + + +def _text(value: str, label: str) -> None: + if not isinstance(value, str): + raise TypeError(f"column {label} must be a string") + try: + value.encode("utf-8") + except UnicodeEncodeError as error: + raise ValueError(f"column {label} must contain valid Unicode") from error + + +@dataclass(frozen=True, kw_only=True) +class ColumnMetadata: + """Absent values are None; empty comments and empty tag maps remain present.""" + + comment: str | None = None + tags: dict[str, str] | None = None + + def __post_init__(self) -> None: + """Require Unicode comments and string maps without normalizing values.""" + if self.comment is not None: + _text(self.comment, "comment") + if self.tags is not None: + if not isinstance(self.tags, dict): + raise TypeError("column tags must be a string map") + for key, value in self.tags.items(): + _text(key, "tag key") + _text(value, "tag value") + + +def _object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError("column tags contain duplicate JSON keys") + result[key] = value + return result + + +def _constant(value: str) -> Any: + raise ValueError(f"column tags contain invalid JSON constant {value}") + + +def column_metadata(field: pa.Field[Any]) -> ColumnMetadata: + """Decode reserved keys strictly while leaving unrelated metadata opaque.""" + # Catalog column metadata already requires unique Arrow keys. This is + # separate from duplicate decoded keys inside the tags JSON object. + if not field.equals(field.with_metadata(cast(Any, field.metadata)), check_metadata=True): + raise ValueError("column metadata keys must be unique") + metadata = field.metadata or {} + try: + comment = metadata[COMMENT_KEY].decode("utf-8") if COMMENT_KEY in metadata else None + tags = ( + json.loads(metadata[TAGS_KEY].decode("utf-8"), object_pairs_hook=_object, parse_constant=_constant) + if TAGS_KEY in metadata + else None + ) + except (UnicodeDecodeError, json.JSONDecodeError, RecursionError) as error: + raise ValueError("column comment/tags metadata requires valid UTF-8 and JSON") from error + if TAGS_KEY in metadata and not isinstance(tags, dict): + raise ValueError("column tags JSON must be an object of strings") + if tags is not None and any(not isinstance(value, str) for value in tags.values()): + raise ValueError("column tags JSON must be an object of strings") + return ColumnMetadata(comment=comment, tags=tags) + + +def with_column_metadata(field: pa.Field[Any], value: ColumnMetadata) -> pa.Field[Any]: + """Replace the two reserved values, preserving all unrelated field metadata.""" + original = column_metadata(field) + value.__post_init__() + metadata = dict(field.metadata or {}) + metadata.pop(COMMENT_KEY, None) + metadata.pop(TAGS_KEY, None) + if value.comment is not None: + metadata[COMMENT_KEY] = value.comment.encode("utf-8") + if value.tags is not None: + tags = ( + (field.metadata or {})[TAGS_KEY] + if value.tags == original.tags + else json.dumps(value.tags, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + metadata[TAGS_KEY] = tags + return field.with_metadata(cast(Any, metadata or None)) + + +def validate_column_metadata(schema: pa.Schema) -> None: + """Validate each field, including container, dictionary and extension children.""" + pending = list(schema) + while pending: + field = pending.pop() + column_metadata(field) + dtype = field.type + while isinstance(dtype, pa.BaseExtensionType) or pa.types.is_dictionary(dtype): + dtype = dtype.storage_type if isinstance(dtype, pa.BaseExtensionType) else dtype.value_type + pending.extend(dtype.field(index) for index in range(dtype.num_fields)) diff --git a/vgi/catalog/constraints.py b/vgi/catalog/constraints.py new file mode 100644 index 00000000..a6248c53 --- /dev/null +++ b/vgi/catalog/constraints.py @@ -0,0 +1,457 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Canonical constraint definitions, identities and atomic mutation envelopes. + +These records validate the wire contract. Providers still own SQL binding, +identifier resolution, authorization, enforcement, revisions and transactions. +No record alone establishes that a backend enforces a constraint. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum +from typing import Annotated, Self + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc.utils import IpcValidation + +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.sql import SqlExpression +from vgi.column_path import ColumnPath, column_path_key, resolve_column_path +from vgi.typed_values import TypedValueBatch + +MAX_CONSTRAINT_BYTES = 8 * 1024 * 1024 +MAX_CONSTRAINT_TOKEN_BYTES = 64 * 1024 +MAX_CONSTRAINT_ITEMS = 1024 +MAX_CONSTRAINT_PATH_DEPTH = 64 +MAX_CONSTRAINT_IDENTIFIER_BYTES = 4096 + + +class ConstraintKind(StrEnum): + """Supported definition variants; each has exactly one payload shape.""" + + NOT_NULL = "NOT_NULL" + UNIQUE = "UNIQUE" + PRIMARY_KEY = "PRIMARY_KEY" + CHECK = "CHECK" + FOREIGN_KEY = "FOREIGN_KEY" + + +class ConstraintEnforcement(StrEnum): + """Whether the authority enforces this constraint on every write path.""" + + ENFORCED = "ENFORCED" + METADATA_ONLY = "METADATA_ONLY" + + +class ConstraintValidation(StrEnum): + """Coverage of existing data; only ALL_ROWS can justify engine facts.""" + + ALL_ROWS = "ALL_ROWS" + FUTURE_WRITES = "FUTURE_WRITES" + UNVALIDATED = "UNVALIDATED" + + +class ForeignKeyMatch(StrEnum): + """Declared NULL matching; FULL requires its negotiated execution profile.""" + + SIMPLE = "SIMPLE" + FULL = "FULL" + + +class ReferentialAction(StrEnum): + """Declared referential action, independently gated by provider support.""" + + NO_ACTION = "NO_ACTION" + RESTRICT = "RESTRICT" + CASCADE = "CASCADE" + SET_NULL = "SET_NULL" + SET_DEFAULT = "SET_DEFAULT" + + +class ConstraintOperation(StrEnum): + """Operations in one atomic constraint mutation request.""" + + ADD = "ADD" + DROP = "DROP" + RENAME = "RENAME" + REPLACE = "REPLACE" + + +def _identifier(value: object, label: str) -> None: + if ( + not isinstance(value, str) + or not value + or "\0" in value + or len(value.encode()) > MAX_CONSTRAINT_IDENTIFIER_BYTES + ): + raise ValueError(f"{label} must be a nonempty bounded identifier without NUL") + + +def _token(value: object, label: str) -> None: + if not isinstance(value, bytes) or not 0 < len(value) <= MAX_CONSTRAINT_TOKEN_BYTES: + raise ValueError(f"{label} must be nonempty bounded bytes") + + +def _reference(value: CatalogObjectRef, kind: str) -> None: + if not isinstance(value, CatalogObjectRef): + raise TypeError("constraint identities require CatalogObjectRef records") + value.__post_init__() + if value.kind != kind: + raise ValueError(f"expected a {kind} reference") + if len(value.schema_path) > MAX_CONSTRAINT_PATH_DEPTH: + raise ValueError("constraint schema path is too deep") + for name in [*value.schema_path, value.name]: + _identifier(name, "object identifier") + if value.owner_table is not None: + _identifier(value.owner_table, "owner table") + + +def _paths(paths: list[ColumnPath], *, nonempty: bool = True) -> None: + if not isinstance(paths, list) or len(paths) > MAX_CONSTRAINT_ITEMS or (nonempty and not paths): + raise ValueError("constraint column paths must be a bounded list with the required arity") + keys = [] + for path in paths: + if not isinstance(path, list) or len(path) > MAX_CONSTRAINT_PATH_DEPTH: + raise ValueError("constraint paths must be bounded lists of raw components") + keys.append(column_path_key(path)) + for name in path: + _identifier(name, "column path component") + if len(set(keys)) != len(keys): + raise ValueError("constraint column paths must be unique") + + +def _same_table(ref: CatalogObjectRef, table: CatalogObjectRef) -> None: + if ref.schema_path != table.schema_path or ref.owner_table != table.name: + raise ValueError("constraint identity belongs to another table") + + +def _definition_owner(definition: ConstraintDefinition, table: CatalogObjectRef) -> None: + if definition.check_expression is not None and definition.check_expression.schema_path != table.schema_path: + raise ValueError("CHECK expression must bind in its owning table schema") + + +def _records(values: list[bytes], *, nonempty: bool = False) -> None: + if not isinstance(values, list) or len(values) > MAX_CONSTRAINT_ITEMS or (nonempty and not values): + raise ValueError("constraint records must be a bounded list with the required length") + if any(not isinstance(value, bytes) or not value for value in values): + raise ValueError("nested constraint records must be nonempty IPC bytes") + if sum(map(len, values)) > MAX_CONSTRAINT_BYTES: + raise ValueError("nested constraint records exceed the envelope limit") + + +class _ConstraintRecord(ArrowSerializableDataclass): + @classmethod + def deserialize_from_bytes(cls, data: bytes, ipc_validation: IpcValidation = IpcValidation.FULL) -> Self: + """Decode one bounded row, rejecting extra batches and trailing bytes.""" + if not isinstance(data, bytes) or not 0 < len(data) <= MAX_CONSTRAINT_BYTES: + raise ValueError("invalid constraint record size") + return cls.deserialize_from_batch(TypedValueBatch.deserialize_from_bytes(data).batch) + + @classmethod + def deserialize_from_batch( + cls, + batch: pa.RecordBatch, + custom_metadata: pa.KeyValueMetadata | None = None, + *, + ipc_validation: IpcValidation = IpcValidation.FULL, + ) -> Self: + """Require the complete schema and validate buffers and nested nullability.""" + if batch.num_rows != 1 or not batch.schema.equals(cls.ARROW_SCHEMA, check_metadata=True): + raise ValueError(f"{cls.__name__} requires one row with its exact schema") + if batch.nbytes > MAX_CONSTRAINT_BYTES: + raise ValueError("constraint record exceeds the envelope limit") + TypedValueBatch(batch) + try: + return super().deserialize_from_batch(batch, custom_metadata, ipc_validation=IpcValidation.FULL) + except KeyError as error: + raise ValueError(f"{cls.__name__} contains an unknown enum or record field") from error + + def serialize_to_bytes(self) -> bytes: + """Revalidate mutable lists and nested definitions before sending bytes.""" + self.__post_init__() + result = super().serialize_to_bytes() + if len(result) > MAX_CONSTRAINT_BYTES: + raise ValueError("constraint record exceeds the envelope limit") + return result + + def __post_init__(self) -> None: + """Validate the concrete record's discriminated payload.""" + + +@dataclass(frozen=True, kw_only=True) +class ForeignKeyDefinition(_ConstraintRecord): + """An attachment-relative FK target and explicit immediate match/actions.""" + + referenced_table: Annotated[CatalogObjectRef, ArrowType(pa.binary())] + referenced_column_paths: list[ColumnPath] + match: ForeignKeyMatch = ForeignKeyMatch.SIMPLE + on_update: ReferentialAction = ReferentialAction.NO_ACTION + on_delete: ReferentialAction = ReferentialAction.NO_ACTION + + def __post_init__(self) -> None: + """Validate the target and ordered referenced key without binding SQL.""" + _reference(self.referenced_table, "table") + _paths(self.referenced_column_paths) + ForeignKeyMatch(self.match) + ReferentialAction(self.on_update) + ReferentialAction(self.on_delete) + + +@dataclass(frozen=True, kw_only=True) +class ConstraintDefinition(_ConstraintRecord): + """Exactly one constraint definition; labels are never mutation identity.""" + + kind: ConstraintKind + column_paths: list[ColumnPath] = field(default_factory=list) + declared_name: str | None = None + check_expression: Annotated[SqlExpression | None, ArrowType(pa.binary())] = None + foreign_key: Annotated[ForeignKeyDefinition | None, ArrowType(pa.binary())] = None + + def __post_init__(self) -> None: + """Reject conflicting payloads, duplicate paths and malformed FK arity.""" + kind = ConstraintKind(self.kind) + if self.declared_name is not None: + _identifier(self.declared_name, "constraint declared name") + _paths(self.column_paths, nonempty=kind != ConstraintKind.CHECK) + if kind == ConstraintKind.NOT_NULL and len(self.column_paths) != 1: + raise ValueError("NOT_NULL requires exactly one column path") + if kind == ConstraintKind.CHECK: + if self.column_paths or not isinstance(self.check_expression, SqlExpression): + raise ValueError("CHECK requires exactly one scalar expression and no column paths") + self.check_expression.__post_init__() + elif self.check_expression is not None: + raise ValueError("only CHECK may carry a check expression") + if kind == ConstraintKind.FOREIGN_KEY: + if not isinstance(self.foreign_key, ForeignKeyDefinition): + raise ValueError("FOREIGN_KEY requires a foreign key definition") + self.foreign_key.__post_init__() + if len(self.column_paths) != len(self.foreign_key.referenced_column_paths): + raise ValueError("foreign key local and referenced arity must match") + elif self.foreign_key is not None: + raise ValueError("only FOREIGN_KEY may carry a foreign key definition") + + def validate_schema(self, schema: pa.Schema) -> None: + """Resolve local raw paths exactly; expression binding belongs to the provider.""" + self.__post_init__() + for path in self.column_paths: + resolve_column_path(schema, path) + + +@dataclass(frozen=True, kw_only=True) +class ConstraintInfo(_ConstraintRecord): + """Canonical readback with stable logical identity and explicit trust state.""" + + ref: Annotated[CatalogObjectRef, ArrowType(pa.binary())] + definition: Annotated[ConstraintDefinition, ArrowType(pa.binary())] + enforcement: ConstraintEnforcement + validation: ConstraintValidation + referenced_constraint: Annotated[CatalogObjectRef | None, ArrowType(pa.binary())] = None + + def __post_init__(self) -> None: + """Verify ownership and the exact resolved referenced-key identity.""" + _reference(self.ref, "constraint") + if not isinstance(self.definition, ConstraintDefinition): + raise TypeError("constraint info requires a typed definition") + self.definition.__post_init__() + ConstraintEnforcement(self.enforcement) + ConstraintValidation(self.validation) + if ( + self.definition.check_expression is not None + and self.definition.check_expression.schema_path != self.ref.schema_path + ): + raise ValueError("CHECK expression must bind in its owning table schema") + if self.definition.kind == ConstraintKind.FOREIGN_KEY: + if self.referenced_constraint is None: + raise ValueError("foreign key readback requires its resolved referenced constraint") + _reference(self.referenced_constraint, "constraint") + assert self.definition.foreign_key is not None + _same_table(self.referenced_constraint, self.definition.foreign_key.referenced_table) + if self.referenced_constraint.key() == self.ref.key(): + raise ValueError("a foreign key cannot reference itself as its unique key") + elif self.referenced_constraint is not None: + raise ValueError("only foreign key readback may reference another constraint") + + @property + def trusted(self) -> bool: + """Whether declared coverage permits engine facts, assuming an honest provider.""" + return self.enforcement == ConstraintEnforcement.ENFORCED and self.validation == ConstraintValidation.ALL_ROWS + + +@dataclass(frozen=True, kw_only=True) +class ConstraintChange(_ConstraintRecord): + """Strict ADD/DROP/RENAME/REPLACE union for one atomic apply request.""" + + operation: ConstraintOperation + ref: Annotated[CatalogObjectRef | None, ArrowType(pa.binary())] = None + definition: Annotated[ConstraintDefinition | None, ArrowType(pa.binary())] = None + enforcement: ConstraintEnforcement | None = None + validation: ConstraintValidation | None = None + cascade: bool | None = None + declared_name: str | None = None + + def __post_init__(self) -> None: + """Require relevant fields and reject every conflicting optional field.""" + operation = ConstraintOperation(self.operation) + if operation == ConstraintOperation.ADD: + if self.ref is not None: + raise ValueError("ADD cannot supply an existing constraint identity") + elif self.ref is None: + raise ValueError("constraint mutation requires an existing identity") + else: + _reference(self.ref, "constraint") + if operation in {ConstraintOperation.ADD, ConstraintOperation.REPLACE}: + if not isinstance(self.definition, ConstraintDefinition): + raise ValueError("ADD/REPLACE requires a typed definition") + self.definition.__post_init__() + if self.enforcement is None or self.validation is None: + raise ValueError("ADD/REPLACE requires explicit enforcement and validation") + ConstraintEnforcement(self.enforcement) + ConstraintValidation(self.validation) + if self.cascade is not None or self.declared_name is not None: + raise ValueError("ADD/REPLACE cannot carry DROP/RENAME fields") + elif self.definition is not None or self.enforcement is not None or self.validation is not None: + raise ValueError("DROP/RENAME cannot carry definition or enforcement fields") + if operation == ConstraintOperation.DROP: + if type(self.cascade) is not bool or self.declared_name is not None: + raise ValueError("DROP requires an explicit cascade boolean and no declared name") + elif self.cascade is not None: + raise ValueError("only DROP may specify cascade") + if operation == ConstraintOperation.RENAME and self.declared_name is not None: + _identifier(self.declared_name, "replacement constraint label") + + +@dataclass(frozen=True, kw_only=True) +class ConstraintSnapshot(_ConstraintRecord): + """Complete constraint readback at one table semantic-definition revision.""" + + table: Annotated[CatalogObjectRef, ArrowType(pa.binary())] + definition_revision: bytes + constraints: list[bytes] + + def __post_init__(self) -> None: + """Reject mixed owners, duplicate stable keys/labels and multiple primary keys.""" + _reference(self.table, "table") + _token(self.definition_revision, "table definition revision") + infos = self.constraint_infos() + keys = set() + labels = set() + primary_keys = 0 + for info in infos: + _same_table(info.ref, self.table) + if info.ref.key() in keys: + raise ValueError("duplicate constraint identity in snapshot") + keys.add(info.ref.key()) + label = info.definition.declared_name + if label is not None: + if label in labels: + raise ValueError("duplicate declared constraint label in snapshot") + labels.add(label) + primary_keys += info.definition.kind == ConstraintKind.PRIMARY_KEY + if primary_keys > 1: + raise ValueError("a table cannot have multiple primary keys") + + def constraint_infos(self) -> list[ConstraintInfo]: + """Decode the bounded ordered readback records without normalizing identities.""" + _records(self.constraints) + return [ConstraintInfo.deserialize_from_bytes(value) for value in self.constraints] + + +@dataclass(frozen=True, kw_only=True) +class ConstraintsApplyRequest(_ConstraintRecord): + """One owner-scoped atomic mutation guarded by an expected table revision.""" + + attach_opaque_data: bytes + table: Annotated[CatalogObjectRef, ArrowType(pa.binary())] + expected_revision: bytes + changes: list[bytes] + transaction_opaque_data: bytes | None = None + + def __post_init__(self) -> None: + """Validate scope and forbid repeated mutation of the same existing key.""" + _token(self.attach_opaque_data, "attachment") + _reference(self.table, "table") + _token(self.expected_revision, "expected table revision") + if self.transaction_opaque_data is not None: + _token(self.transaction_opaque_data, "transaction") + keys = set() + for change in self.constraint_changes(): + if change.ref is not None: + _same_table(change.ref, self.table) + if change.ref.key() in keys: + raise ValueError("one request cannot address an existing constraint key twice") + keys.add(change.ref.key()) + if change.definition is not None: + _definition_owner(change.definition, self.table) + + def constraint_changes(self) -> list[ConstraintChange]: + """Decode all ordered changes before an authority attempts effects.""" + _records(self.changes, nonempty=True) + return [ConstraintChange.deserialize_from_bytes(value) for value in self.changes] + + +def constraint_definitions(values: list[bytes], table: CatalogObjectRef) -> list[ConstraintDefinition]: + """Decode the authoritative CREATE list, without inventing names or trust.""" + _reference(table, "table") + _records(values) + definitions = [ConstraintDefinition.deserialize_from_bytes(value) for value in values] + labels = [definition.declared_name for definition in definitions if definition.declared_name is not None] + if len(labels) != len(set(labels)): + raise ValueError("duplicate declared constraint label") + if sum(definition.kind == ConstraintKind.PRIMARY_KEY for definition in definitions) > 1: + raise ValueError("a table cannot have multiple primary keys") + for definition in definitions: + _definition_owner(definition, table) + return definitions + + +def constraint_infos(values: list[bytes], table: CatalogObjectRef, revision: bytes | None) -> list[ConstraintInfo]: + """Validate canonical discovery without requiring mutation support.""" + if revision is not None: + _token(revision, "table definition revision") + # Snapshot validation is identical except that read-only discovery may + # legitimately lack a revision. The placeholder is local validation only. + snapshot = ConstraintSnapshot(table=table, definition_revision=revision or b"read-only", constraints=values) + return snapshot.constraint_infos() + + +def validate_constraint_schema( + schema: pa.Schema, + definitions: list[ConstraintDefinition], + *, + trusted: list[ConstraintDefinition] | None = None, +) -> None: + """Require exact raw paths and agreement between trust and Arrow nullability.""" + for definition in definitions: + definition.validate_schema(schema) + required = { + tuple(path) + for definition in (definitions if trusted is None else trusted) + if definition.kind in {ConstraintKind.NOT_NULL, ConstraintKind.PRIMARY_KEY} + for path in definition.column_paths + } + visible_paths: set[tuple[str, ...]] = set() + + def visit(fields: pa.Schema | pa.StructType, prefix: tuple[str, ...]) -> None: + for column_field in fields: + if not prefix and b"is_row_id" in (column_field.metadata or {}): + # Storage identity is not a user-visible SQL constraint target. + continue + path = (*prefix, column_field.name) + visible_paths.add(path) + if column_field.nullable == (path in required): + raise ValueError("Arrow nullability must agree with enforced ALL_ROWS NOT_NULL/PRIMARY constraints") + metadata = column_field.metadata or {} + # A logical type's storage members are not table column paths. + # In particular Parquet Variant requires a nonnullable metadata + # child without declaring a SQL NOT NULL constraint on that child. + if pa.types.is_struct(column_field.type) and not any( + key == b"ARROW:extension:name" or key.startswith(b"vgi.logical_type.") for key in metadata + ): + visit(column_field.type, path) + + visit(schema, ()) + if any(tuple(path) not in visible_paths for definition in definitions for path in definition.column_paths): + raise ValueError("constraint paths cannot address storage identities or logical-type storage members") diff --git a/vgi/catalog/contents_codec.py b/vgi/catalog/contents_codec.py index af1e8597..e3e6bb3f 100644 --- a/vgi/catalog/contents_codec.py +++ b/vgi/catalog/contents_codec.py @@ -1,4 +1,4 @@ -"""Catalog-wide metadata collections for the 2.2 wire contract. +"""Catalog-wide metadata collections using the six-stream codec introduced in 2.2. The author-facing inventory stays grouped by schema. Collection encoders reuse the ordinary record conversion, and readers consume every row of every batch. @@ -212,6 +212,7 @@ def decode_inventory( def contents_digest(categories: dict[str, bytes | None]) -> str: """Hash category identity and complete stream bytes with unambiguous lengths.""" + # This domain versions the unchanged stream framing, not the VGI protocol. digest = hashlib.sha256(b"vgi.catalog.contents.2.2\0") for kind in CATEGORY_TYPES: data = categories[kind] diff --git a/vgi/catalog/dependencies.py b/vgi/catalog/dependencies.py new file mode 100644 index 00000000..55414bca --- /dev/null +++ b/vgi/catalog/dependencies.py @@ -0,0 +1,504 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Attachment-relative object identities and atomic dependency-drop planning.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field, replace +from typing import Literal, Self, TypeAlias + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.utils import IpcValidation + +from vgi.column_path import column_path_key, resolve_column_path +from vgi.typed_values import TypedValueBatch + +ObjectKey: TypeAlias = tuple[str, tuple[str, ...], str, str] # noqa: UP040 +ColumnKey: TypeAlias = tuple[str, ...] # noqa: UP040 +ScopedKey: TypeAlias = tuple[ObjectKey, ColumnKey | None] # noqa: UP040 +AlterAction: TypeAlias = Literal["rename", "type", "drop"] # noqa: UP040 + + +class _DependencyRecord(ArrowSerializableDataclass): + def serialize_to_bytes(self) -> bytes: + """Revalidate mutable path lists before writing a dependency record.""" + self.__post_init__() + return super().serialize_to_bytes() + + def __post_init__(self) -> None: + """Validate the concrete record.""" + + @classmethod + def deserialize_from_bytes(cls, data: bytes, ipc_validation: IpcValidation = IpcValidation.FULL) -> Self: + """Require one complete record, including all nested IPC records.""" + return cls.deserialize_from_batch(TypedValueBatch.deserialize_from_bytes(data).batch) + + @classmethod + def deserialize_from_batch( + cls, + batch: pa.RecordBatch, + custom_metadata: pa.KeyValueMetadata | None = None, + *, + ipc_validation: IpcValidation = IpcValidation.FULL, + ) -> Self: + """Reject missing fields, incorrect nullability and extra rows.""" + if batch.num_rows != 1 or not batch.schema.equals(cls.ARROW_SCHEMA, check_metadata=False): + raise ValueError(f"{cls.__name__} requires one row with its complete schema") + if any(batch.column(i).null_count for i, f in enumerate(batch.schema) if not f.nullable): + raise ValueError(f"{cls.__name__} contains NULL in a required field") + return super().deserialize_from_batch(batch, custom_metadata, ipc_validation=IpcValidation.FULL) + + +def _identifier(value: object) -> None: + if not isinstance(value, str) or not value or "\0" in value: + raise ValueError("object identifiers must be nonempty strings without NUL") + + +@dataclass(frozen=True, kw_only=True) +class CatalogObjectRef(_DependencyRecord): + """A resolved object in this attachment, without a client catalog alias. + + For a schema, schema_path names its parents and name is its final component. + Other objects use their owning schema path. Table-owned triggers and + constraints require owner_table. Names are raw components; a provider + returns canonical spelling and resolves request identifiers according to + its declared name policy. + Function and macro references identify the complete overload family. + For constraints, name is a persisted provider-issued logical key compared + exactly, never a SQL label or engine OID; declared-name changes retain it. + """ + + kind: str + schema_path: list[str] + name: str + owner_table: str | None = None + + def __post_init__(self) -> None: + """Validate the namespace without flattening or case-folding names.""" + if self.kind not in { + "schema", + "table", + "view", + "index", + "scalar_macro", + "table_macro", + "sequence", + "type", + "scalar_function", + "aggregate_function", + "table_function", + "window_function", + "trigger", + "constraint", + }: + raise ValueError("unknown catalog object kind") + if not isinstance(self.schema_path, list): + raise ValueError("schema_path must be a list of raw identifiers") + if not self.schema_path and self.kind != "schema": + raise ValueError("non-schema objects require an owning schema") + for component in [*self.schema_path, self.name]: + _identifier(component) + if self.kind in {"trigger", "constraint"}: + _identifier(self.owner_table) + elif self.owner_table is not None: + raise ValueError("only triggers and constraints have a table-owned namespace") + + def key(self) -> ObjectKey: + """Return the full resolved identity, retaining component boundaries.""" + self.__post_init__() + return self.kind, tuple(self.schema_path), self.name, self.owner_table or "" + + +@dataclass(frozen=True, kw_only=True) +class ObjectDependency(_DependencyRecord): + """An edge from dependent to subject, both CatalogObjectRef IPC records. + + ``requires`` blocks RESTRICT; CASCADE deletes the dependent. ``owned`` + automatically deletes the dependent when its owner is dropped, even under + RESTRICT. Dropping a dependent never drops its subject. Owned objects can + be dropped explicitly, have at most one owner, and cannot form owner cycles. + Schema membership and parent schemas use requires, not owned. + """ + + dependent: bytes + subject: bytes + blocks_alter: bool + kind: str = "requires" + subject_column_paths: list[list[str]] | None = None + dependent_column_path: list[str] | None = None + + def __post_init__(self) -> None: + """Validate structural identities; schemas must prove actual columns.""" + if self.kind not in {"requires", "owned"}: + raise ValueError("unknown dependency kind") + if type(self.blocks_alter) is not bool: + raise ValueError("blocks_alter must be a boolean") + dependent, subject = self.dependent_ref(), self.subject_ref() + if self.dependent_column_path is not None: + _column_key(self.dependent_column_path) + if dependent.kind != "table": + raise ValueError("dependent column paths require a table reference") + if self.subject_column_paths is not None: + if not isinstance(self.subject_column_paths, list) or not self.subject_column_paths: + raise ValueError("subject column paths must be a nonempty list or NULL") + paths = [_column_key(path) for path in self.subject_column_paths] + if len(set(paths)) != len(paths): + raise ValueError("subject column paths must be unique") + if subject.kind not in {"table", "view"}: + raise ValueError("subject column paths require a table or view reference") + if dependent.key() == subject.key() and ( + self.dependent_column_path is None + or self.subject_column_paths is None + or any(_overlaps(tuple(self.dependent_column_path), tuple(path)) for path in self.subject_column_paths) + ): + raise ValueError("self dependencies require distinct nonoverlapping column targets") + + def dependent_ref(self) -> CatalogObjectRef: + """Decode the dependent's resolved identity.""" + return CatalogObjectRef.deserialize_from_bytes(self.dependent) + + def subject_ref(self) -> CatalogObjectRef: + """Decode the subject's resolved identity.""" + return CatalogObjectRef.deserialize_from_bytes(self.subject) + + def identity(self) -> tuple[ObjectKey, ColumnKey | None, ObjectKey]: + """Return edge identity, excluding the mergeable subject path set.""" + return ( + self.dependent_ref().key(), + None if self.dependent_column_path is None else _column_key(self.dependent_column_path), + self.subject_ref().key(), + ) + + +def _column_key(path: list[str]) -> ColumnKey: + if not isinstance(path, list): + raise ValueError("column paths must be lists of raw identifiers") + return column_path_key(path) + + +def _overlaps(left: ColumnKey | None, right: ColumnKey | None) -> bool: + return left is None or right is None or left[: len(right)] == right or right[: len(left)] == left + + +def _scope_sort_key(scope: ScopedKey) -> tuple[ObjectKey, bool, ColumnKey]: + return scope[0], scope[1] is not None, scope[1] or () + + +def merge_dependencies(edges: list[ObjectDependency]) -> list[ObjectDependency]: + """Canonicalize identical edges, merging only their protected subject paths. + + NULL protects the whole subject and dominates a narrower set. Explicit + parent and child paths are retained so schema validation can prove every + claimed field. Different dependent columns remain separate edges, and + conflicting kind/ALTER promises are invalid. + """ + merged: dict[tuple[ObjectKey, ColumnKey | None, ObjectKey], ObjectDependency] = {} + for edge in edges: + edge.__post_init__() + identity = edge.identity() + previous = merged.get(identity) + if previous is not None and (previous.kind != edge.kind or previous.blocks_alter != edge.blocks_alter): + raise ValueError("conflicting dependency kind or blocks_alter flag") + paths = edge.subject_column_paths + if previous is not None: + if previous.subject_column_paths is None or paths is None: + paths = None + else: + paths = previous.subject_column_paths + paths + if paths is not None: + keys = sorted({tuple(path) for path in paths}) + paths = [list(key) for key in keys] + merged[identity] = replace(edge, subject_column_paths=paths) + return sorted( + merged.values(), + key=lambda edge: (_scope_sort_key((edge.identity()[0], edge.identity()[1])), edge.identity()[2]), + ) + + +@dataclass(frozen=True, kw_only=True) +class ColumnDrop: + """One column deletion; it never grants permission to delete its table.""" + + table: CatalogObjectRef + column_path: list[str] + + +@dataclass(frozen=True, kw_only=True) +class SemanticAlterPlan: + """An advisory mutation closure, requiring provider authorization and atomicity. + + Rename/type plans have no deletion side effects. Column DROP separates + objects and columns so a generated-column edge cannot erase its table. + The provider must validate schemas, rewrite metadata, and authorize every + returned deletion against its transaction-visible graph before effects. + """ + + target: CatalogObjectRef + column_path: list[str] | None + action: AlterAction + drop_objects: list[CatalogObjectRef] = field(default_factory=list) + drop_columns: list[ColumnDrop] = field(default_factory=list) + + +def _dependency_cycles(edges: list[ObjectDependency], *, ownership_only: bool) -> None: + selected = [edge for edge in edges if not ownership_only or edge.kind == "owned"] + nodes = {(edge.identity()[0], edge.identity()[1]) for edge in selected} + adjacency: dict[ScopedKey, set[ScopedKey]] = {node: set() for node in nodes} + for edge in selected: + dependent, path, subject = edge.identity() + subjects: list[ColumnKey | None] = ( + [None] if edge.subject_column_paths is None else [tuple(path) for path in edge.subject_column_paths] + ) + adjacency[(dependent, path)].update( + node + for node in nodes + if node[0] == subject + and any( + _overlaps(node[1], path) + and (ownership_only or path is None or node[1] is not None or subject[0] != "table") + for path in subjects + ) + ) + # A table column reads a column value, not every whole-table lifetime + # dependency. Otherwise T.g -> T.a would enter an unrelated legal + # T -> index -> T requires/owned cycle. View columns still depend on + # the whole view definition; ownership validation always follows the + # full lifetime relationship, regardless of protected column paths. + for start in nodes: + # Object-only reference cycles (including backing-index requires/owned + # pairs) are valid. Any column participating in a cycle is invalid. + if not ownership_only and start[1] is None: + continue + pending = list(adjacency[start]) + visited = set() + while pending: + current = pending.pop() + if current == start: + raise ValueError("ownership cycle" if ownership_only else "column dependency cycle") + if current not in visited: + visited.add(current) + pending.extend(adjacency[current] - visited) + + +@dataclass(frozen=True, kw_only=True) +class DependencySnapshot(_DependencyRecord): + """One complete, transaction-visible graph at a catalog metadata version. + + Objects and edges are IPC records. The snapshot is advisory to clients: + mutations recheck the provider's authoritative graph atomically. A provider + unable to disclose the complete graph rejects this read profile. + """ + + catalog_version: int + objects: list[bytes] = field(default_factory=list) + dependencies: list[bytes] = field(default_factory=list) + + def __post_init__(self) -> None: + """Merge edge paths; reject dangling identities, conflicting policies and cycles.""" + if type(self.catalog_version) is not int or not 0 <= self.catalog_version < 2**63: + raise ValueError("catalog_version must be a nonnegative int64") + refs = self.object_refs() + keys = {ref.key() for ref in refs} + if len(keys) != len(refs): + raise ValueError("duplicate object identity") + edges = merge_dependencies(self.edges()) + owners: dict[ScopedKey, ObjectKey] = {} + for edge in edges: + dependent, column, subject = edge.identity() + if dependent not in keys or subject not in keys: + raise ValueError("dangling dependency endpoint") + if edge.kind == "owned": + if (dependent, column) in owners: + raise ValueError("an object cannot have multiple owners") + owners[(dependent, column)] = subject + _dependency_cycles(edges, ownership_only=True) + _dependency_cycles(edges, ownership_only=False) + object.__setattr__(self, "dependencies", [edge.serialize_to_bytes() for edge in edges]) + + def object_refs(self) -> list[CatalogObjectRef]: + """Decode every object, including objects with no dependencies.""" + if not isinstance(self.objects, list): + raise ValueError("objects must be a list") + return [CatalogObjectRef.deserialize_from_bytes(value) for value in self.objects] + + def edges(self) -> list[ObjectDependency]: + """Decode every dependency edge.""" + if not isinstance(self.dependencies, list): + raise ValueError("dependencies must be a list") + return [ObjectDependency.deserialize_from_bytes(value) for value in self.dependencies] + + def drop_set(self, target: CatalogObjectRef, *, cascade: bool = False) -> list[CatalogObjectRef]: + """Plan a complete deletion set without effects, independent of edge order. + + Find the closure of automatic drops first, then check blocking edges. + CASCADE follows both kinds; reference cycles terminate through the set. + The provider authorizes every deletion before applying any of them. + """ + plan = self.plan_alter(target, action="drop", cascade=cascade) + if plan.drop_columns: + raise ValueError("object-only drop_set cannot express dependent column drops; use plan_alter") + return plan.drop_objects + + def validate_columns(self, schemas: Mapping[ObjectKey, pa.Schema]) -> None: + """Prove all claimed columns using authoritative transaction-visible schemas. + + The snapshot contains no schemas. A structurally valid edge therefore + does not prove that its columns exist; callers supplying schemas must + include every object with column-scoped references. + """ + self.__post_init__() + for edge in self.edges(): + dependent, path, subject = edge.identity() + claims = [] + if path is not None: + claims.append((dependent, path)) + if edge.subject_column_paths is not None: + claims.extend((subject, tuple(path)) for path in edge.subject_column_paths) + for key, column in claims: + schema = schemas.get(key) + if schema is None: + raise ValueError("authoritative schema required for every dependency column") + resolve_column_path(schema, column) + + def _alter_target( + self, + target: CatalogObjectRef, + column_path: list[str] | None, + action: AlterAction, + schemas: Mapping[ObjectKey, pa.Schema] | None, + ) -> ScopedKey: + self.__post_init__() + if action not in {"rename", "type", "drop"}: + raise ValueError("unknown semantic ALTER action") + if target.key() not in {ref.key() for ref in self.object_refs()}: + raise ValueError("catalog object does not exist") + path = None if column_path is None else _column_key(column_path) + if path is not None and target.kind != "table": + raise ValueError("semantic column ALTER requires a table reference") + if schemas is not None: + self.validate_columns(schemas) + if path is not None: + schema = schemas.get(target.key()) + if schema is None: + raise ValueError("authoritative schema required for the ALTER column") + resolve_column_path(schema, path) + return target.key(), path + + def alter_blockers( + self, + target: CatalogObjectRef, + *, + column_path: list[str] | None = None, + action: AlterAction = "rename", + schemas: Mapping[ObjectKey, pa.Schema] | None = None, + ) -> list[ObjectDependency]: + """Return directly affected edges, preserving raw parent/child protection. + + Rename/type changes honor blocks_alter. DROP must resolve every affected + edge regardless of that flag. An outgoing generated-column dependency + does not block dropping that generated column itself. + """ + subject, path = self._alter_target(target, column_path, action, schemas) + return [ + edge + for edge in self.edges() + if edge.subject_ref().key() == subject + and (action == "drop" or edge.blocks_alter) + and ( + edge.subject_column_paths is None + or any(_overlaps(path, tuple(protected)) for protected in edge.subject_column_paths) + ) + ] + + def assert_alter_allowed( + self, + target: CatalogObjectRef, + *, + column_path: list[str] | None = None, + action: AlterAction = "rename", + schemas: Mapping[ObjectKey, pa.Schema] | None = None, + ) -> None: + """Reject a semantic change that would require resolving dependencies. + + This checks an isolated alteration only. DROP with automatic ownership + cleanup or CASCADE requires plan_alter and authorization of its closure. + """ + if self.alter_blockers(target, column_path=column_path, action=action, schemas=schemas): + raise ValueError("cannot alter object or column because dependent objects or columns exist") + + def plan_alter( + self, + target: CatalogObjectRef, + *, + column_path: list[str] | None = None, + action: AlterAction, + cascade: bool = False, + schemas: Mapping[ObjectKey, pa.Schema] | None = None, + ) -> SemanticAlterPlan: + """Plan rename/type blockers or a complete object/column DROP closure. + + Owned deletions expand before RESTRICT blockers are checked. CASCADE + follows both kinds. All DROP edges apply even when blocks_alter is + false. Parent column drops cover their descendants, while a dependent + column never expands into deletion of its entire table. + """ + start = self._alter_target(target, column_path, action, schemas) + if type(cascade) is not bool: + raise ValueError("cascade must be a boolean") + if action != "drop": + self.assert_alter_allowed(target, column_path=column_path, action=action, schemas=schemas) + return SemanticAlterPlan(target=target, column_path=column_path, action=action) + objects = {ref.key(): ref for ref in self.object_refs()} + dropping = {start} + edges = self.edges() + + def deleted(scope: ScopedKey) -> bool: + return any( + key == scope[0] and (path is None or scope[1] is not None and scope[1][: len(path)] == path) + for key, path in dropping + ) + + def affected(edge: ObjectDependency) -> bool: + subject = edge.subject_ref().key() + paths: list[ColumnKey | None] = ( + [None] if edge.subject_column_paths is None else [tuple(path) for path in edge.subject_column_paths] + ) + return any( + key == subject and any(_overlaps(path, protected) for protected in paths) for key, path in dropping + ) + + while True: + additional = { + (edge.identity()[0], edge.identity()[1]) + for edge in edges + if affected(edge) and (cascade or edge.kind == "owned") + } + if additional <= dropping: + break + dropping.update(additional) + if any(affected(edge) and not deleted((edge.identity()[0], edge.identity()[1])) for edge in edges): + raise ValueError("cannot drop object or column because dependent objects or columns exist; use CASCADE") + # A whole-object deletion or a parent column subsumes smaller deletions. + minimal = { + scope + for scope in dropping + if not any( + other != scope + and other[0] == scope[0] + and (other[1] is None or scope[1] is not None and scope[1][: len(other[1])] == other[1]) + for other in dropping + ) + } + return SemanticAlterPlan( + target=target, + column_path=None if column_path is None else list(column_path), + action=action, + drop_objects=[objects[key] for key, path in sorted(minimal, key=_scope_sort_key) if path is None], + drop_columns=[ + ColumnDrop(table=objects[key], column_path=list(path)) + for key, path in sorted(minimal, key=_scope_sort_key) + if path is not None + ], + ) diff --git a/vgi/catalog/descriptors.py b/vgi/catalog/descriptors.py index 840f3050..d2e9e1cb 100644 --- a/vgi/catalog/descriptors.py +++ b/vgi/catalog/descriptors.py @@ -21,11 +21,11 @@ from vgi.argument_spec import macro_arguments_schema from vgi.arguments import Arguments +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION from vgi.catalog.catalog_interface import ( AttachOpaqueData, ColumnStatistics, ForeignKeyInfo, - IndexConstraintType, IndexInfo, MacroInfo, MacroType, @@ -38,9 +38,17 @@ scan_arguments_from, serialize_column_statistics, ) +from vgi.catalog.column_metadata import column_metadata, with_column_metadata +from vgi.catalog.constraints import ConstraintInfo +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.indexes import IndexDefinition +from vgi.catalog.macros import normalize_macro_overloads +from vgi.catalog.options import OptionState +from vgi.catalog.sql import ColumnExpressions, SqlExpression, column_expressions, with_column_expressions +from vgi.column_path import RequiredFilterPaths, column_path_key from vgi.invocation import BindResponse, FunctionType from vgi.metadata import CatalogFunctionType -from vgi.schema_path import SchemaPath, schema_path_display, schema_path_key +from vgi.schema_path import SchemaPath, schema_path_display, schema_path_key, validate_default_schema from vgi.write_results import validate_write_result_modes if TYPE_CHECKING: @@ -272,7 +280,10 @@ class Table: write_result_modes: Maximum result mode for each configured write function. Omitted operations default to ``count``. Modes are ordered ``count < rows < changes``. - not_null: Tuple of column names with NOT NULL constraints. + constraints: Provider-issued canonical constraint records, including explicit + identity, enforcement and validation state. + definition_revision: Opaque table-definition revision for mutation. + not_null: Legacy declarations; nonempty values require explicit migration. unique: Tuple of column name tuples for UNIQUE constraints. check: Tuple of SQL expressions for CHECK constraints. primary_key: Tuple of column-name tuples forming the primary key. At @@ -280,7 +291,7 @@ class Table: foreign_key: Tuple of [`ForeignKeyDef`][] foreign-key constraints. defaults: Dict mapping column names to default values. Accepts Python literals (str, int, float, bool, None) which are - auto-converted, or `SqlExpression` for raw SQL. + auto-converted, or `Sql` for raw SQL. generated_columns: Dict mapping column names to SQL expressions for generated (virtual) columns. Generated columns are computed on read by DuckDB and are mutually exclusive with @@ -288,21 +299,13 @@ class Table: column_comments: Dict mapping column names to comment strings. Comments are transported as Arrow field metadata and visible via ``duckdb_columns()`` in DuckDB. - required_filters: Required WHERE-filter groups in conjunctive normal - form — an AND (outer tuple) of OR-groups (inner tuples) of dotted-path - column references that MUST appear in a WHERE expression for any scan - of this table. A group is satisfied when any one of its paths has a - filter; every group must be satisfied. So - ``(("accession_number",), ("ticker", "cik"))`` means "accession_number - AND one of (ticker, cik)"; a single-path group ``(("country",),)`` is - a plain mandatory filter. Paths are top-level names (``"country"``) or - struct subfields (``"bbox.xmin"``, ``"nested.outer.inner"``). Empty - (default) means no enforcement. Satisfaction is prefix-based: a present - filter on a shorter path satisfies any required path it is a prefix of - (a whole-struct filter on ``bbox`` satisfies all of ``bbox.xmin`` / - ``.xmax`` / ``.ymin`` / ``.ymax``). The VGI DuckDB extension's - optimizer pass consults this at bind time and throws - ``BinderException`` listing any unsatisfied groups. + required_filters: AND-of-OR groups of raw component paths, e.g. + ``((("bbox", "xmin"),), (("ticker",), ("cik",)))``. Dots and quotes + are literal characters. Every path must resolve in ``columns``. + A filter on a parent struct satisfies requirements on its children; + a child filter does not satisfy a requirement on its parent. + Empty means no enforcement. Use ``RequiredFilterPaths.from_legacy`` + explicitly when migrating old dotted strings; ambiguous names fail. statistics: Mapping of column name to ``ColumnStatisticsInput`` providing inlined column statistics for the optimizer. statistics_cache_max_age_seconds: How long clients may cache the inlined @@ -337,6 +340,8 @@ class Table: update_function: type[TableInOutGenerator[Any, Any]] | None = None delete_function: type[TableInOutGenerator[Any, Any]] | None = None write_result_modes: dict[str, str] = field(default_factory=dict) + constraints: tuple[ConstraintInfo, ...] = () + definition_revision: bytes | None = None not_null: tuple[str, ...] = () unique: tuple[tuple[str, ...], ...] = () check: tuple[str, ...] = () @@ -345,7 +350,7 @@ class Table: defaults: dict[str, DefaultValue] = field(default_factory=dict) generated_columns: dict[str, str] = field(default_factory=dict) column_comments: dict[str, str] = field(default_factory=dict) - required_filters: tuple[tuple[str, ...], ...] = () + required_filters: tuple[tuple[tuple[str, ...], ...], ...] = () statistics: dict[str, ColumnStatisticsInput] = field(default_factory=dict) statistics_cache_max_age_seconds: int | None = None cardinality_estimate: int | None = None @@ -488,22 +493,11 @@ def __post_init__(self) -> None: f"in schema. Available columns: {sorted(column_names)}" ) - # Validate required_filters: each OR-group must be non-empty, and the - # leading dotted segment of each path must be a real column on this table. - # Struct subfield validity is not checked here — DuckDB's binder catches - # typos at scan time, and the descriptor doesn't unpack STRUCT subfields. - for group in self.required_filters: - if not group: - raise ValueError(f"Table '{self.name}': required_filters must not contain empty groups") - for path in group: - if not path: - raise ValueError(f"Table '{self.name}': required_filters must not contain empty strings") - head = path.split(".", 1)[0] - if head not in column_names: - raise ValueError( - f"Table '{self.name}': required_filters path '{path}' references " - f"unknown column '{head}'. Available columns: {sorted(column_names)}" - ) + # Validate full raw paths; never reinterpret dotted names. + required = RequiredFilterPaths( + groups=[[list(column_path_key(path)) for path in group] for group in self.required_filters] + ) + required.validate_schema(resolved) # Validate statistics column names for col in self.statistics: @@ -613,16 +607,26 @@ def _apply_defaults_to_schema(self, schema: pa.Schema) -> pa.Schema: schema = schema.set(idx, f.with_metadata(existing)) # type: ignore[arg-type] return schema - def _apply_generated_columns_to_schema(self, schema: pa.Schema) -> pa.Schema: + def _apply_generated_columns_to_schema(self, schema: pa.Schema, schema_path: SchemaPath) -> pa.Schema: """Return schema with generated expression metadata applied to fields.""" if not self.generated_columns: return schema for col_name, expression in self.generated_columns.items(): idx = schema.get_field_index(col_name) f = schema.field(idx) - existing = dict(f.metadata) if f.metadata else {} - existing[b"generated_expression"] = expression.encode("utf-8") - schema = schema.set(idx, f.with_metadata(existing)) # type: ignore[arg-type] + existing = column_expressions(f) + generated = SqlExpression( + sql=expression, + profile=ROW_SCALAR_EXPRESSION, + schema_path=list(schema_path), + evaluation_context=QueryEvaluationContext(), + ) + schema = schema.set( + idx, + with_column_expressions( + f, ColumnExpressions(default=existing.default, generated=generated, generation="virtual") + ), + ) return schema def _apply_column_comments_to_schema(self, schema: pa.Schema) -> pa.Schema: @@ -630,19 +634,17 @@ def _apply_column_comments_to_schema(self, schema: pa.Schema) -> pa.Schema: if not self.column_comments: return schema for col_name, comment in self.column_comments.items(): - if not comment: - continue idx = schema.get_field_index(col_name) f = schema.field(idx) - existing = dict(f.metadata) if f.metadata else {} - existing[b"comment"] = comment.encode("utf-8") - schema = schema.set(idx, f.with_metadata(existing)) # type: ignore[arg-type] + schema = schema.set(idx, with_column_metadata(f, replace(column_metadata(f), comment=comment))) return schema def to_table_info(self, schema_path: SchemaPath) -> TableInfo: """Convert to [`TableInfo`][] for catalog response.""" + if any((self.not_null, self.unique, self.check, self.primary_key, self.foreign_key)): + raise ValueError("legacy constraint declarations require explicit canonical ConstraintInfo records") cols = self._apply_defaults_to_schema(self.resolved_columns) - cols = self._apply_generated_columns_to_schema(cols) + cols = self._apply_generated_columns_to_schema(cols, schema_path) cols = self._apply_column_comments_to_schema(cols) # Inline the resolved stats blob so the C++ extension can short-circuit # the per-scan ``table_function_statistics`` and per-table @@ -663,11 +665,8 @@ def to_table_info(self, schema_path: SchemaPath) -> TableInfo: name=self.name, schema_path=schema_path, columns=SerializedSchema(cols.serialize().to_pybytes()), - not_null_constraints=self._resolve_not_null_indices(), - unique_constraints=self._resolve_unique_indices(), - check_constraints=list(self.check), - primary_key_constraints=self._resolve_primary_key_indices(), - foreign_key_constraints=self._serialize_foreign_keys(schema_path), + constraints=[info.serialize_to_bytes() for info in self.constraints], + definition_revision=self.definition_revision, write_result_modes={ operation: self.write_result_modes.get(operation, "count") for operation, function in ( @@ -687,7 +686,7 @@ def to_table_info(self, schema_path: SchemaPath) -> TableInfo: cardinality_estimate=self.cardinality_estimate, cardinality_max=self.cardinality_max, column_statistics=column_statistics_blob, - required_filters=[list(group) for group in self.required_filters], + required_filters=[[list(path) for path in group] for group in self.required_filters], ) def resolve_column_statistics(self) -> TableColumnStatisticsResult | None: @@ -725,6 +724,9 @@ class View: from the view's query, so only the names that actually appear in the result need entries; unmatched names are ignored. tags: Optional metadata tags. + aliases: Ordered raw output identifiers, including any literal dots or quotes. + security: ``regular`` or ``barrier``; an executing reader must enforce + the optimization barrier or reject the view. """ @@ -733,6 +735,8 @@ class View: comment: str | None = None column_comments: dict[str, str] = field(default_factory=dict) tags: dict[str, str] = field(default_factory=dict) + aliases: list[str] = field(default_factory=list) + security: str = "regular" def to_view_info(self, schema_path: SchemaPath) -> ViewInfo: """Convert to [`ViewInfo`][] for catalog response.""" @@ -740,6 +744,8 @@ def to_view_info(self, schema_path: SchemaPath) -> ViewInfo: name=self.name, schema_path=schema_path, definition=self.definition, + aliases=list(self.aliases), + security=self.security, comment=self.comment, column_comments=dict(self.column_comments), tags=dict(self.tags), @@ -758,13 +764,12 @@ class Macro: names and values are typed defaults. None if no defaults. Example: pa.RecordBatch.from_pydict({"b": [5]}) for b := 5. parameter_docs: Optional mapping of parameter name to a human/agent-facing - description. Keys must appear in ``parameters``. Descriptions flow over - the wire via the macro ``arguments_schema``'s ``vgi_doc`` field metadata - (the same channel functions use), so the DuckDB extension's - ``vgi_function_arguments()`` can surface them. Empty/default = no docs. + description. Keys must appear in ``parameters``. Descriptions become + canonical MacroParameter documentation. Empty/default = no docs. definition: SQL expression (scalar) or query (table). comment: Optional macro comment. tags: Optional metadata tags. + evaluation_context: Explicit owner context used by the local single-definition adapter. """ @@ -777,6 +782,8 @@ class Macro: comment: str | None = None tags: dict[str, str] = field(default_factory=dict) + evaluation_context: QueryEvaluationContext = field(default_factory=QueryEvaluationContext) + def __post_init__(self) -> None: """Validate macro configuration.""" param_set = set(self.parameters) @@ -802,70 +809,44 @@ def __post_init__(self) -> None: ) def to_macro_info(self, schema_path: SchemaPath) -> MacroInfo: - """Convert to [`MacroInfo`][] for catalog response.""" + """Normalize the local single declaration into its sole wire representation.""" return MacroInfo( name=self.name, schema_path=schema_path, macro_type=self.macro_type, - parameters=list(self.parameters), - parameter_default_values=self.parameter_default_values, - definition=self.definition, + overloads=normalize_macro_overloads( + macro_type=self.macro_type.value, + schema_path=schema_path, + parameters=list(self.parameters), + parameter_default_values=self.parameter_default_values, + definition=self.definition, + arguments_schema=macro_arguments_schema( + self.parameters, self.parameter_default_values, self.parameter_docs + ), + evaluation_context=self.evaluation_context, + ), comment=self.comment, tags=dict(self.tags), - arguments_schema=macro_arguments_schema( - self.parameters, - self.parameter_default_values, - self.parameter_docs, - ), ) @dataclass(frozen=True, slots=True, kw_only=True) class Index: - """Declarative index definition. - - Immutable. - - Attributes: - name: Index name. - table_name: Name of the table this index is on. - expressions: SQL expression strings or column names defining the index. - For column-based indexes: ("col_a", "col_b") - For expression indexes: ("lower(col_a)", "col_b + 1") - index_type: The index type (e.g., "" for default). - constraint_type: NONE for regular, UNIQUE for unique indexes. - options: Key-value index options. - comment: Optional index comment. - tags: Optional metadata tags. - - """ + """Explicit resolved index declaration; no physical index is inferred.""" name: str - table_name: str - expressions: tuple[str, ...] = () - index_type: str = "" - constraint_type: IndexConstraintType = IndexConstraintType.NONE - options: dict[str, str] = field(default_factory=dict) + definition: IndexDefinition + options: OptionState comment: str | None = None tags: dict[str, str] = field(default_factory=dict) - def __post_init__(self) -> None: - """Validate index configuration.""" - if not self.expressions: - raise ValueError(f"Index '{self.name}': must specify at least one expression") - if not self.table_name: - raise ValueError(f"Index '{self.name}': must specify a table_name") - def to_index_info(self, schema_path: SchemaPath) -> IndexInfo: - """Convert to [`IndexInfo`][] for catalog response.""" + """Publish the exact typed definition and effective option provenance.""" return IndexInfo( name=self.name, schema_path=schema_path, - table_name=self.table_name, - index_type=self.index_type, - constraint_type=self.constraint_type, - expressions=list(self.expressions), - options=dict(self.options), + definition=self.definition, + options=self.options, comment=self.comment, tags=dict(self.tags), ) @@ -951,7 +932,7 @@ class Catalog: Attributes: name: The catalog name (used in SQL as the database name). - default_schema: Schema to use for unqualified table/view/function names. + default_schema: Raw root schema name for unqualified names, or None for no implicit default. schemas: Sequence of Schema objects defining the catalog contents. comment: Optional comment describing the catalog. tags: Optional key-value tags associated with the catalog. @@ -984,7 +965,7 @@ class Catalog: """ name: str - default_schema: str = "main" + default_schema: str | None = "main" schemas: Sequence[Schema] = () comment: str | None = None tags: dict[str, str] = field(default_factory=dict) @@ -996,8 +977,9 @@ def __post_init__(self) -> None: """Validate catalog configuration.""" schema_paths = {schema_path_key(s.path) for s in self.schemas} - # Validate default_schema exists - if (self.default_schema.lower(),) not in schema_paths: + # A present default names one root; dots are literal identifier text. + validate_default_schema(self.default_schema) + if self.default_schema is not None and (self.default_schema.lower(),) not in schema_paths: available = sorted(schema_path_display(s.path) for s in self.schemas) or ["(none)"] raise ValueError( f"Catalog '{self.name}': default_schema '{self.default_schema}' " diff --git a/vgi/catalog/evaluation_context.py b/vgi/catalog/evaluation_context.py new file mode 100644 index 00000000..f6caee11 --- /dev/null +++ b/vgi/catalog/evaluation_context.py @@ -0,0 +1,105 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Shared SQL evaluation settings and bounded Arrow record envelopes.""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from typing import Self + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.utils import IpcValidation + +from vgi.typed_values import TypedValueBatch + +MAX_SQL_RECORD_BYTES = 8 * 1024 * 1024 + + +def _text(value: str, label: str, *, maximum: int = 4096, empty: bool = False) -> None: + if not isinstance(value, str) or "\0" in value or (not empty and not value): + raise ValueError(f"invalid {label}") + if len(value.encode("utf-8")) > maximum: + raise ValueError(f"{label} is too large") + + +class _SqlRecord(ArrowSerializableDataclass): + @classmethod + def deserialize_from_bytes(cls, data: bytes, ipc_validation: IpcValidation = IpcValidation.FULL) -> Self: + """Bound the envelope before nested decoding and require one complete row.""" + if not isinstance(data, bytes) or not 0 < len(data) <= MAX_SQL_RECORD_BYTES: + raise ValueError("invalid SQL record size") + return cls.deserialize_from_batch(TypedValueBatch.deserialize_from_bytes(data).batch) + + @classmethod + def deserialize_from_batch( + cls, + batch: pa.RecordBatch, + custom_metadata: pa.KeyValueMetadata | None = None, + *, + ipc_validation: IpcValidation = IpcValidation.FULL, + ) -> Self: + """Validate full Arrow buffers, required fields and exact field definitions.""" + if batch.num_rows != 1 or not batch.schema.equals(cls.ARROW_SCHEMA, check_metadata=False): + raise ValueError(f"{cls.__name__} requires one row with its complete schema") + if batch.nbytes > MAX_SQL_RECORD_BYTES: + raise ValueError("SQL record is too large") + TypedValueBatch(batch) # Includes required nested fields, not just Arrow buffer validity. + return super().deserialize_from_batch(batch, custom_metadata, ipc_validation=IpcValidation.FULL) + + def serialize_to_bytes(self) -> bytes: + """Enforce the same envelope bound for locally constructed requests.""" + if validate := getattr(self, "__post_init__", None): + validate() + result = super().serialize_to_bytes() + if len(result) > MAX_SQL_RECORD_BYTES: + raise ValueError("SQL record is too large") + return result + + +@dataclass(frozen=True, kw_only=True) +class QueryEvaluationContext(_SqlRecord): + """The complete versioned set of query-evaluation settings on the wire.""" + + timezone: str = "UTC" + collation: str = "binary" + default_order: str = "asc" + default_null_order: str = "nulls_last" + preserve_insertion_order: bool = True + + def __post_init__(self) -> None: + """Reject unknown enum values; providers certify supported contexts.""" + _text(self.timezone, "query timezone", maximum=256) + _text(self.collation, "query collation", maximum=256) + if self.default_order not in {"asc", "desc"}: + raise ValueError("unknown default query order") + if self.default_null_order not in { + "nulls_first", + "nulls_last", + "nulls_first_on_asc_last_on_desc", + "nulls_last_on_asc_first_on_desc", + }: + raise ValueError("unknown default NULL order") + if type(self.preserve_insertion_order) is not bool: + raise ValueError("preserve_insertion_order must be a boolean") + + def digest(self, encoding: str, semantic_profile: str) -> bytes: + """Hash the documented length-prefixed UTF-8 fields, independently of IPC.""" + self.__post_init__() + _text(encoding, "query encoding", maximum=256) + _text(semantic_profile, "query profile", maximum=256) + payload = bytearray(b"vgi.query.context.v1\0") + for value in [ + encoding, + semantic_profile, + self.timezone, + self.collation, + self.default_order, + self.default_null_order, + ]: + encoded = value.encode("utf-8") + payload.extend(len(encoded).to_bytes(4, "big")) + payload.extend(encoded) + payload.append(int(self.preserve_insertion_order)) + return hashlib.sha256(payload).digest() diff --git a/vgi/catalog/indexes.py b/vgi/catalog/indexes.py new file mode 100644 index 00000000..a0231166 --- /dev/null +++ b/vgi/catalog/indexes.py @@ -0,0 +1,92 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Typed index definitions; providers own binding, collation and physical indexes.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import Annotated + +import pyarrow as pa +from vgi_rpc import ArrowType + +from vgi.catalog.constraints import _ConstraintRecord, _identifier, _paths, _records, _reference +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.sql import SqlExpression +from vgi.column_path import ColumnPath +from vgi.typed_values import TypedValueBatch + + +class IndexConstraintType(Enum): + """Declared index semantics; UNIQUE does not imply a logical PRIMARY key.""" + + NONE = "none" + UNIQUE = "unique" + PRIMARY = "primary" + + +@dataclass(frozen=True, kw_only=True) +class IndexKey(_ConstraintRecord): + """Exactly one raw column path or explicitly contextual SQL expression. + + Raw keys require provider-established binary column collation in the initial + profile. Direction and physical ordering require an algorithm profile. + """ + + column_path: ColumnPath | None = None + expression: Annotated[SqlExpression | None, ArrowType(pa.binary())] = None + + def __post_init__(self) -> None: + """Reject absent or ambiguous keys without interpreting SQL strings.""" + if (self.column_path is None) == (self.expression is None): + raise ValueError("index key requires exactly one column path or expression") + if self.column_path is not None: + _paths([self.column_path]) + if self.expression is not None: + self.expression.__post_init__() + + +@dataclass(frozen=True, kw_only=True) +class IndexDefinition(_ConstraintRecord): + """Complete typed index creation and resolved discovery definition. + + Options are typed literals. Discovery returns effective option values here + and in IndexInfo.options, whose explicit_names preserve stored overrides. + No algorithm, ordering, collation or constraint association is inferred. + """ + + table: Annotated[CatalogObjectRef, ArrowType(pa.binary())] + algorithm: str + keys: list[bytes] + options: Annotated[TypedValueBatch, ArrowType(pa.binary())] + constraint_type: IndexConstraintType + associated_constraint: Annotated[CatalogObjectRef | None, ArrowType(pa.binary())] = None + + def __post_init__(self) -> None: + """Validate the complete record before provider binding or publication.""" + _reference(self.table, "table") + _identifier(self.algorithm, "index algorithm") + _records(self.keys, nonempty=True) + for key in self.key_definitions(): + if key.expression is not None and key.expression.schema_path != self.table.schema_path: + raise ValueError("index expression must bind in its table schema") + if not isinstance(self.options, TypedValueBatch): + raise TypeError("index options require TypedValueBatch") + self.options.__post_init__() + kind = IndexConstraintType(self.constraint_type) + if kind == IndexConstraintType.PRIMARY and self.associated_constraint is None: + raise ValueError("PRIMARY index requires an authoritative constraint association") + if self.associated_constraint is not None: + _reference(self.associated_constraint, "constraint") + if ( + self.associated_constraint.schema_path != self.table.schema_path + or self.associated_constraint.owner_table != self.table.name + ): + raise ValueError("associated constraint belongs to another table") + if kind == IndexConstraintType.NONE: + raise ValueError("regular index cannot claim a key constraint association") + + def key_definitions(self) -> list[IndexKey]: + """Decode keys in their declared order without splitting raw names.""" + return [IndexKey.deserialize_from_bytes(value) for value in self.keys] diff --git a/vgi/catalog/layout.py b/vgi/catalog/layout.py new file mode 100644 index 00000000..57962906 --- /dev/null +++ b/vgi/catalog/layout.py @@ -0,0 +1,124 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Portable table-layout definitions; they never promise scan output ordering.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum +from typing import Annotated + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType + +from vgi.catalog.sql import SqlExpression as SqlExpression + + +class SortDirection(StrEnum): + """Explicit sort direction, independent of the caller's session defaults.""" + + ASC = "asc" + DESC = "desc" + + +class NullOrder(StrEnum): + """Explicit NULL position for a sort key.""" + + FIRST = "first" + LAST = "last" + + +@dataclass(frozen=True, kw_only=True) +class SortKey(ArrowSerializableDataclass): + """Scalar key, direction and NULL order for a table layout.""" + + expression: Annotated[SqlExpression, ArrowType(pa.binary())] + direction: SortDirection + null_order: NullOrder + + def __post_init__(self) -> None: + """Validate the expression and reject session-dependent order values.""" + self.expression.__post_init__() + SortDirection(self.direction) + NullOrder(self.null_order) + + +class LayoutCoverage(StrEnum): + """Whether existing data has been rewritten for the current definition.""" + + ALL_ROWS = "all_rows" + FUTURE_WRITES = "future_writes" + + +@dataclass(frozen=True, kw_only=True) +class TableLayout(ArrowSerializableDataclass): + """Stored layout policy, with ordered lists of IPC expression/key records. + + Empty lists mean no partitioning/sorting policy. ``coverage`` describes + physical layout applicability, not relational constraints or scan order. + A create request uses ALL_ROWS because the new table initially has no rows. + """ + + partition_by: Annotated[list[bytes], ArrowType(pa.list_(pa.binary()))] = field(default_factory=list) + sorted_by: Annotated[list[bytes], ArrowType(pa.list_(pa.binary()))] = field(default_factory=list) + coverage: LayoutCoverage = LayoutCoverage.ALL_ROWS + + def __post_init__(self) -> None: + """Validate every nested record before provider execution.""" + self.partition_expressions() + self.sort_keys() + LayoutCoverage(self.coverage) + + def partition_expressions(self) -> list[SqlExpression]: + """Decode ordered partition expressions.""" + return [SqlExpression.deserialize_from_bytes(value) for value in self.partition_by] + + def sort_keys(self) -> list[SortKey]: + """Decode ordered sort keys without discarding direction or NULL order.""" + return [SortKey.deserialize_from_bytes(value) for value in self.sorted_by] + + @classmethod + def from_expressions(cls, *, partition_by: list[SqlExpression], sorted_by: list[SortKey]) -> TableLayout: + """Build a creation layout from typed expression/key objects.""" + return cls( + partition_by=[expr.serialize_to_bytes() for expr in partition_by], + sorted_by=[key.serialize_to_bytes() for key in sorted_by], + ) + + +class LayoutChangeMode(StrEnum): + """Explicit existing-data policy for an atomic layout alteration.""" + + FUTURE_WRITES = "future_writes" + REWRITE = "rewrite" + + +@dataclass(frozen=True, kw_only=True) +class TableLayoutChange(ArrowSerializableDataclass): + """Replace either layout component, atomically under the requested mode. + + NULL leaves a component unchanged; an empty list clears it. REWRITE succeeds + only after all existing data conforms to the replacement layout. A provider + that cannot honor the mode must reject it before mutation. No implicit retry. + """ + + mode: LayoutChangeMode + partition_by: Annotated[list[bytes] | None, ArrowType(pa.list_(pa.binary()))] = None + sorted_by: Annotated[list[bytes] | None, ArrowType(pa.list_(pa.binary()))] = None + + def __post_init__(self) -> None: + """Reject empty changes and validate all supplied definitions.""" + LayoutChangeMode(self.mode) + if self.partition_by is None and self.sorted_by is None: + raise ValueError("layout change must replace at least one component") + TableLayout(partition_by=self.partition_by or [], sorted_by=self.sorted_by or []) + + def apply_to(self, current: TableLayout) -> TableLayout: + """Build a candidate; the provider must implement the requested rewrite.""" + self.__post_init__() + current.__post_init__() + return TableLayout( + partition_by=current.partition_by if self.partition_by is None else self.partition_by, + sorted_by=current.sorted_by if self.sorted_by is None else self.sorted_by, + coverage=LayoutCoverage.ALL_ROWS if self.mode == LayoutChangeMode.REWRITE else LayoutCoverage.FUTURE_WRITES, + ) diff --git a/vgi/catalog/macros.py b/vgi/catalog/macros.py new file mode 100644 index 00000000..d2455106 --- /dev/null +++ b/vgi/catalog/macros.py @@ -0,0 +1,352 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Complete macro overload families, independent of engine-private ASTs. + +Declaration order is preserved; the receiving SQL binder owns overload +resolution, coercions and ambiguity errors. Legacy documentation schemas never +declare parameter types: their types describe literal defaults only. +""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, replace +from typing import Annotated, Any, Self, cast + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc.utils import IpcValidation + +from vgi.argument_spec import macro_arguments_schema, macro_parameter_docs_from_schema +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.layout import SqlExpression +from vgi.schema_path import SchemaPath, schema_path_key +from vgi.typed_values import TypedValueBatch + +MACRO_PROFILE = "duckdb.macro.v1" +MACRO_DEFAULT_PROFILE = "duckdb.macro.default.v1" +MAX_MACRO_BYTES = 8 * 1024 * 1024 +MAX_MACRO_OVERLOADS = 256 +MAX_MACRO_PARAMETERS = 1024 +_ASCII_LOWER = str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") + + +def _name(name: str) -> None: + if not isinstance(name, str) or not name or "\0" in name or len(name.encode()) > 4096: + raise ValueError("macro identifiers must be nonempty, bounded strings without NUL") + + +class _MacroRecord(ArrowSerializableDataclass): + @classmethod + def deserialize_from_bytes(cls, data: bytes, ipc_validation: IpcValidation = IpcValidation.FULL) -> Self: + """Reject multiple rows, partial schemas and oversized envelopes.""" + if not isinstance(data, bytes) or not 0 < len(data) <= MAX_MACRO_BYTES: + raise ValueError("invalid macro record size") + return cls.deserialize_from_batch(TypedValueBatch.deserialize_from_bytes(data).batch) + + @classmethod + def deserialize_from_batch( + cls, + batch: pa.RecordBatch, + custom_metadata: pa.KeyValueMetadata | None = None, + *, + ipc_validation: IpcValidation = IpcValidation.FULL, + ) -> Self: + """Validate the complete Arrow shape and required nested fields.""" + if batch.num_rows != 1 or not batch.schema.equals(cls.ARROW_SCHEMA, check_metadata=False): + raise ValueError(f"{cls.__name__} requires one row with its complete schema") + if batch.nbytes > MAX_MACRO_BYTES: + raise ValueError("macro record is too large") + TypedValueBatch(batch) + return super().deserialize_from_batch(batch, custom_metadata, ipc_validation=IpcValidation.FULL) + + def serialize_to_bytes(self) -> bytes: + """Revalidate mutable collections before serializing a complete record.""" + if validate := getattr(self, "__post_init__", None): + validate() + result = super().serialize_to_bytes() + if len(result) > MAX_MACRO_BYTES: + raise ValueError("macro record is too large") + return result + + +@dataclass(frozen=True, kw_only=True) +class MacroParameter(_MacroRecord): + """One parameter with independent declared type and optional default. + + ``declared_type`` is absent for an untyped parameter; otherwise it is a + singleton schema with this exact name and nullable field. Its recursive + metadata is logical type identity, not documentation. Literal default NULL + is a present one-row value. A parameter cannot have both default forms. + """ + + name: str + declared_type: Annotated[pa.Schema | None, ArrowType(pa.binary())] = None + default_value: Annotated[TypedValueBatch | None, ArrowType(pa.binary())] = None + default_expression: Annotated[SqlExpression | None, ArrowType(pa.binary())] = None + documentation: str | None = None + + def __post_init__(self) -> None: + """Preserve absence, typed NULL and explicit declaration separately.""" + _name(self.name) + if self.declared_type is not None: + if not isinstance(self.declared_type, pa.Schema) or self.declared_type.names != [self.name]: + raise ValueError("declared macro type must be a singleton schema named for its parameter") + if not self.declared_type.field(0).nullable or pa.types.is_null(self.declared_type.field(0).type): + raise ValueError("declared macro type must be nullable and cannot be the null placeholder type") + if self.declared_type.serialize().size > 1024 * 1024: + raise ValueError("declared macro type schema is too large") + if self.default_value is not None: + if not isinstance(self.default_value, TypedValueBatch): + raise TypeError("macro literal defaults require TypedValueBatch") + self.default_value.__post_init__() + if self.default_value.schema.names != [self.name]: + raise ValueError("macro literal default must contain exactly its named parameter") + if self.default_value.batch.nbytes > MAX_MACRO_BYTES: + raise ValueError("macro literal default is too large") + if self.default_expression is not None: + if not isinstance(self.default_expression, SqlExpression): + raise TypeError("macro expression defaults require SqlExpression") + self.default_expression.__post_init__() + if self.default_expression.profile != MACRO_DEFAULT_PROFILE: + raise ValueError("unsupported macro default expression profile") + if self.default_value is not None and self.default_expression is not None: + raise ValueError("macro default value and default expression are mutually exclusive") + if self.documentation is not None and ( + not isinstance(self.documentation, str) + or "\0" in self.documentation + or len(self.documentation.encode()) > 64 * 1024 + ): + raise ValueError("invalid macro parameter documentation") + + @property + def has_default(self) -> bool: + """Distinguish required parameters from any present default, including NULL.""" + return self.default_value is not None or self.default_expression is not None + + +@dataclass(frozen=True, kw_only=True) +class MacroOverload(_MacroRecord): + """One scalar expression or table query in a complete overload family. + + ``parameters`` includes required and defaulted parameters in declaration + order. ``schema_path`` is raw and attachment-relative; it never contains the + client's catalog alias. No overload index is a stable remote object handle. + """ + + parameters: list[MacroParameter] + definition: str + profile: str + schema_path: SchemaPath + macro_type: str + evaluation_context: QueryEvaluationContext + + def __post_init__(self) -> None: + """Validate declaration ordering and the selected semantic profile.""" + if self.macro_type not in {"scalar", "table"}: + raise ValueError("macro_type must be scalar or table") + if self.profile != MACRO_PROFILE: + raise ValueError("unsupported macro definition profile") + if not isinstance(self.evaluation_context, QueryEvaluationContext): + raise TypeError("macro overload requires an explicit evaluation context") + self.evaluation_context.__post_init__() + schema_path_key(self.schema_path) + if len(self.schema_path) > 32: + raise ValueError("macro owner schema is too deep") + for name in self.schema_path: + _name(name) + if not isinstance(self.definition, str) or not self.definition.strip() or "\0" in self.definition: + raise ValueError("macro definition must be nonempty SQL without NUL") + if len(self.definition.encode()) > 1024 * 1024: + raise ValueError("macro definition is too large") + if not isinstance(self.parameters, list) or len(self.parameters) > MAX_MACRO_PARAMETERS: + raise ValueError("invalid macro parameter collection") + names: set[str] = set() + seen_default = False + for parameter in self.parameters: + if not isinstance(parameter, MacroParameter): + raise TypeError("macro parameters require MacroParameter records") + parameter.__post_init__() + key = parameter.name.translate(_ASCII_LOWER) + if key in names: + raise ValueError("macro parameter names must be unique under ASCII case folding") + names.add(key) + if seen_default and not parameter.has_default: + raise ValueError("required macro parameter cannot follow a defaulted parameter") + seen_default |= parameter.has_default + if ( + parameter.default_expression is not None + and parameter.default_expression.schema_path != self.schema_path + ): + raise ValueError("macro default expression must use its overload owner schema") + if ( + parameter.default_expression is not None + and parameter.default_expression.evaluation_context != self.evaluation_context + ): + raise ValueError("macro default expression must use its overload evaluation context") + + +def validate_macro_overloads( + *, + macro_type: str, + schema_path: SchemaPath, + parameters: list[str], + definition: str, + parameter_default_values: pa.RecordBatch | None = None, + arguments_schema: pa.Schema | None = None, + overloads: list[MacroOverload] | None = None, +) -> None: + """Reject ambiguous dual forms and validate the complete replacement family. + + ``None`` retains the existing legacy contract. A new collection is never a + patch and must not carry a duplicate first overload in legacy fields. + """ + if overloads is None: + return + if not isinstance(overloads, list) or not 1 <= len(overloads) <= MAX_MACRO_OVERLOADS: + raise ValueError("macro overloads must be a nonempty bounded collection") + if parameters != [] or definition != "" or parameter_default_values is not None or arguments_schema is not None: + raise ValueError("macro overload collection conflicts with legacy single-definition fields") + for overload in overloads: + if not isinstance(overload, MacroOverload): + raise TypeError("macro overload collection requires MacroOverload records") + overload.__post_init__() + if overload.macro_type != macro_type or overload.schema_path != schema_path: + raise ValueError("macro overload kind and owner must equal the parent macro") + + +def validate_macro_family(*, macro_type: str, schema_path: SchemaPath, overloads: list[MacroOverload]) -> None: + """Validate the sole wire representation, including mandatory nonempty family.""" + if overloads is None: + raise ValueError("macro overloads must be a nonempty bounded collection") + validate_macro_overloads( + macro_type=macro_type, schema_path=schema_path, parameters=[], definition="", overloads=overloads + ) + + +def normalize_macro_overloads( + *, + macro_type: str, + schema_path: SchemaPath, + parameters: list[str], + definition: str, + parameter_default_values: pa.RecordBatch | None = None, + arguments_schema: pa.Schema | None = None, + overloads: list[MacroOverload] | None = None, + evaluation_context: QueryEvaluationContext | None = None, +) -> list[MacroOverload]: + """Upgrade a legacy definition without inventing declared parameter types. + + Unknown legacy documentation metadata is rejected rather than discarded. + New collections are copied through the actual wire codec, which validates + and prevents later caller mutation from changing a stored definition. + """ + validate_macro_overloads( + macro_type=macro_type, + schema_path=schema_path, + parameters=parameters, + definition=definition, + parameter_default_values=parameter_default_values, + arguments_schema=arguments_schema, + overloads=overloads, + ) + if overloads is not None: + return [MacroOverload.deserialize_from_bytes(item.serialize_to_bytes()) for item in overloads] + if not isinstance(evaluation_context, QueryEvaluationContext): + raise ValueError("legacy macro upgrade requires the owner's evaluation context") + if parameter_default_values is not None: + TypedValueBatch(parameter_default_values) + if any(name not in parameters for name in parameter_default_values.schema.names): + raise ValueError("legacy macro defaults name an unknown parameter") + docs = macro_parameter_docs_from_schema(arguments_schema) if arguments_schema is not None else {} + if arguments_schema is not None and not arguments_schema.equals( + macro_arguments_schema(parameters, parameter_default_values, docs), check_metadata=True + ): + raise ValueError("legacy arguments_schema has unsupported or inconsistent documentation metadata") + result = MacroOverload( + evaluation_context=evaluation_context, + macro_type=macro_type, + schema_path=list(schema_path), + profile=MACRO_PROFILE, + definition=definition, + parameters=[ + MacroParameter( + name=name, + documentation=docs.get(name), + default_value=TypedValueBatch(parameter_default_values.select([name])) + if parameter_default_values is not None and name in parameter_default_values.schema.names + else None, + ) + for name in parameters + ], + ) + return [result] + + +def legacy_macro_fields( + overloads: list[MacroOverload], *, evaluation_context: QueryEvaluationContext | None = None +) -> dict[str, Any]: + """Return a lossless legacy definition, or reject unsupported information. + + This compatibility helper never chooses the first overload, evaluates an + expression, erases a declaration, or converts Arrow values to SQL strings. + """ + if len(overloads) != 1: + raise ValueError("legacy macros cannot represent multiple overloads") + overload = overloads[0] + overload.__post_init__() + if evaluation_context != overload.evaluation_context: + raise ValueError("legacy macros require a matching explicit destination evaluation context") + if any(parameter.declared_type is not None for parameter in overload.parameters): + raise ValueError("legacy macros cannot represent declared parameter types") + if any(parameter.default_expression is not None for parameter in overload.parameters): + raise ValueError("legacy macros cannot represent expression defaults") + if any(parameter.documentation == "" for parameter in overload.parameters): + raise ValueError("legacy macros cannot preserve explicitly empty documentation") + defaults = [parameter.default_value for parameter in overload.parameters if parameter.default_value is not None] + default_batch = None + if defaults: + source_metadata = defaults[0].schema.metadata + metadata = cast("dict[bytes | str, bytes | str] | None", source_metadata) + if any(default.schema.metadata != metadata for default in defaults): + raise ValueError("legacy macro defaults cannot merge different schema metadata") + default_batch = pa.RecordBatch.from_arrays( + [default.batch.column(0) for default in defaults], + schema=pa.schema([default.schema.field(0) for default in defaults], metadata=metadata), + ) + names = [parameter.name for parameter in overload.parameters] + docs = {parameter.name: parameter.documentation for parameter in overload.parameters if parameter.documentation} + return { + "macro_type": overload.macro_type, + "schema_path": list(overload.schema_path), + "parameters": names, + "definition": overload.definition, + "parameter_default_values": default_batch, + "arguments_schema": macro_arguments_schema(names, default_batch, docs), + } + + +def canonicalize_macro_defaults( + overloads: list[MacroOverload], + resolve: Callable[[MacroOverload, MacroParameter], TypedValueBatch], +) -> list[MacroOverload]: + """Resolve defaults into a new complete family before catalog publication. + + The owning SQL binder supplies ``resolve``: it must reject nonconstant or + unsupported expressions, apply declared-type coercions, and return the + resolved literal. This helper neither runs SQL nor mutates the original. + If any default fails, no candidate family is returned for publication. + """ + if not 1 <= len(overloads) <= MAX_MACRO_OVERLOADS: + raise ValueError("macro overloads must be a nonempty bounded collection") + result = [] + for original in overloads: + overload = MacroOverload.deserialize_from_bytes(original.serialize_to_bytes()) + parameters = [ + replace(parameter, default_value=resolve(overload, parameter), default_expression=None) + if parameter.has_default + else parameter + for parameter in overload.parameters + ] + result.append(replace(overload, parameters=parameters)) + return result diff --git a/vgi/catalog/metadata_changes.py b/vgi/catalog/metadata_changes.py new file mode 100644 index 00000000..7d228a80 --- /dev/null +++ b/vgi/catalog/metadata_changes.py @@ -0,0 +1,88 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Atomic changes to catalog comments and literal string tags.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Self + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.utils import IpcValidation + +from vgi.typed_values import TypedValueBatch + + +@dataclass(frozen=True, kw_only=True) +class CatalogMetadataChanges(ArrowSerializableDataclass): + """A reusable patch with explicit comment presence and per-key tag changes. + + ``set_comment=False`` leaves the comment unchanged and requires ``comment`` + to be NULL. With ``set_comment=True``, NULL clears the comment; an empty + string stores an empty comment. Tag keys are literal, case-sensitive strings. + Setting an empty value differs from removing a key. SET/REMOVE overlap and + duplicate removals are invalid, including when the target does not exist. + """ + + set_comment: bool = False + comment: str | None = None + set_tags: dict[str, str] = field(default_factory=dict) + remove_tags: list[str] = field(default_factory=list) + + def __post_init__(self) -> None: + """Validate the entire patch before constructing a candidate state.""" + if type(self.set_comment) is not bool: + raise ValueError("set_comment must be a non-null boolean") + if self.comment is not None and not isinstance(self.comment, str): + raise ValueError("comment must be a string or NULL") + if not self.set_comment and self.comment is not None: + raise ValueError("comment requires set_comment=true") + if not isinstance(self.set_tags, dict) or any( + not isinstance(k, str) or not isinstance(v, str) for k, v in self.set_tags.items() + ): + raise ValueError("set_tags must map literal strings to strings") + if not isinstance(self.remove_tags, list) or any(not isinstance(k, str) for k in self.remove_tags): + raise ValueError("remove_tags must be a list of literal strings") + if len(set(self.remove_tags)) != len(self.remove_tags): + raise ValueError("remove_tags must contain unique keys") + if self.set_tags.keys() & set(self.remove_tags): + raise ValueError("tags cannot be both SET and REMOVE") + + @classmethod + def deserialize_from_bytes(cls, data: bytes, ipc_validation: IpcValidation = IpcValidation.FULL) -> Self: + """Reject extra batches/trailing bytes and fully validate the Arrow payload.""" + batch = TypedValueBatch.deserialize_from_bytes(data).batch + return cls.deserialize_from_batch(batch, ipc_validation=ipc_validation) + + @classmethod + def deserialize_from_batch( + cls, + batch: pa.RecordBatch, + custom_metadata: pa.KeyValueMetadata | None = None, + *, + ipc_validation: IpcValidation = IpcValidation.FULL, + ) -> Self: + """Require complete patch fields and reject duplicate map keys on the wire.""" + if batch.num_rows != 1 or not batch.schema.equals(cls.ARROW_SCHEMA, check_metadata=False): + raise ValueError("CatalogMetadataChanges requires exactly one row with the complete patch schema") + if any(batch.column(name).null_count for name in ("set_comment", "set_tags", "remove_tags")): + raise ValueError("CatalogMetadataChanges patch fields must be non-null") + pairs = batch.column("set_tags")[0].as_py() + keys = [key for key, _ in pairs] + if len(set(keys)) != len(keys): + raise ValueError("set_tags must contain unique keys") + return super().deserialize_from_batch(batch, custom_metadata, ipc_validation=ipc_validation) + + def apply_to(self, comment: str | None, tags: dict[str, str]) -> tuple[str | None, dict[str, str]]: + """Return a candidate without modifying the supplied state. + + The provider validates authorization and target existence, applies this + candidate in the same transaction snapshot, preserves other object + fields, and advances its catalog version only when metadata changes. + Missing removal keys and an empty patch are no-ops on an existing target. + """ + self.__post_init__() + result = {key: value for key, value in tags.items() if key not in self.remove_tags} + result.update(self.set_tags) + return (self.comment if self.set_comment else comment), result diff --git a/vgi/catalog/options.py b/vgi/catalog/options.py new file mode 100644 index 00000000..19382315 --- /dev/null +++ b/vgi/catalog/options.py @@ -0,0 +1,219 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Typed catalog options with distinct SET, SET NULL, omission, and RESET. + +The provider supplies the accepted Arrow schema and a consistent snapshot of +its defaults. Helpers validate and construct a candidate state without effects; +the provider commits that state under its advertised transaction guarantees. +""" + +from __future__ import annotations + +from collections.abc import Collection +from dataclasses import dataclass, field +from typing import Annotated, Any + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType + +from vgi.typed_values import TypedValueBatch + + +def _validate_names(names: list[str], label: str) -> None: + if not isinstance(names, list) or any(not isinstance(name, str) for name in names): + raise TypeError(f"{label} must be a list of option names") + if any(not name or "\x00" in name for name in names): + raise ValueError(f"{label} names must be nonempty and contain no NUL") + if len(set(names)) != len(names): + raise ValueError(f"{label} names must be unique") + + +@dataclass(frozen=True, kw_only=True) +class OptionDefinitions(ArrowSerializableDataclass): + """Provider option schema, transaction-snapshot defaults and mutability. + + Every declared option has a value. An option without a default is required + at creation, even if nullable, and cannot be RESET. ``create_names`` and + ``alter_names`` are independent allowlists; omission never grants mutation. + Expressions are outside this literal-value contract. + """ + + schema: Annotated[pa.Schema, ArrowType(pa.binary())] + defaults: Annotated[TypedValueBatch, ArrowType(pa.binary())] + create_names: list[str] = field(default_factory=list) + alter_names: list[str] = field(default_factory=list) + + def __post_init__(self) -> None: + """Validate complete definitions, including required creation values.""" + _validate_names(self.schema.names, "declared option") + names = set(self.schema.names) + for label, allowed in (("creation", self.create_names), ("alteration", self.alter_names)): + _validate_names(allowed, label) + if set(allowed) - names: + raise ValueError(f"{label} option names are not declared") + self._validate_subset(self.defaults) + if names - set(self.defaults.schema.names) - set(self.create_names): + raise ValueError("options without defaults must be settable at creation") + + def _validate_subset(self, values: TypedValueBatch) -> None: + unknown = set(values.schema.names) - set(self.schema.names) + if unknown: + raise ValueError(f"unknown option names: {sorted(unknown)}") + values.validate_schema( + pa.schema([self.schema.field(name) for name in values.schema.names]).with_metadata(self.schema.metadata) + ) + + def create(self, values: TypedValueBatch | None = None) -> OptionState: + """Resolve all required values and defaults before a provider creates an object.""" + self.__post_init__() + sets = set(values.schema.names) if values is not None else set() + if values is not None: + self._validate_subset(values) + forbidden = sets - set(self.create_names) + if forbidden: + raise ValueError(f"options cannot be set at creation: {sorted(forbidden)}") + missing = set(self.schema.names) - sets - set(self.defaults.schema.names) + if missing: + raise ValueError(f"required options were not supplied: {sorted(missing)}") + arrays = [ + values.batch.column(name) if values is not None and name in sets else self.defaults.batch.column(name) + for name in self.schema.names + ] + return self._state(arrays, sets) + + def apply(self, current: OptionState, changes: OptionChanges) -> OptionState: + """Validate SET/RESET together and return an effective candidate state.""" + self.__post_init__() + current.__post_init__() + changes.__post_init__() + current.values.validate_schema(self.schema) + values = changes.set_values + sets = set(values.schema.names) if values is not None else set() + if values is not None: + self._validate_subset(values) + resets = set(changes.reset_names) + forbidden = (sets | resets) - set(self.alter_names) + if forbidden: + raise ValueError(f"options are not mutable: {sorted(forbidden)}") + if resets - set(self.defaults.schema.names): + raise ValueError("options without defaults cannot be RESET") + arrays = [ + values.batch.column(name) + if values is not None and name in sets + else self.defaults.batch.column(name) + if name in resets + else current.values.batch.column(name) + for name in self.schema.names + ] + return self._state(arrays, (set(current.explicit_names) | sets) - resets) + + def _state(self, arrays: list[pa.Array[Any]], explicit: set[str]) -> OptionState: + values = ( + TypedValueBatch(pa.RecordBatch.from_arrays(arrays, schema=self.schema)) + if arrays + else TypedValueBatch.empty(metadata=self.schema.metadata) + ) + return OptionState(values=values, explicit_names=[name for name in self.schema.names if name in explicit]) + + +@dataclass(frozen=True, kw_only=True) +class OptionState(ArrowSerializableDataclass): + """Readback of effective option values and which values were explicitly set. + + Attributes: + values: One typed value for every option exposed by this provider. + explicit_names: Names with a stored override, including explicit NULLs + and values equal to the default. Names absent here use defaults. + """ + + values: Annotated[TypedValueBatch, ArrowType(pa.binary())] + explicit_names: list[str] = field(default_factory=list) + + def __post_init__(self) -> None: + """Require explicit names to reference the effective value schema.""" + _validate_names(self.explicit_names, "explicit option") + unknown = set(self.explicit_names) - set(self.values.schema.names) + if unknown: + raise ValueError(f"explicit option names are absent from values: {sorted(unknown)}") + + +@dataclass(frozen=True, kw_only=True) +class OptionChanges(ArrowSerializableDataclass): + """An atomic candidate change to literal catalog options. + + All identifiers are canonical provider option names. Every supplied field + is a SET, including a typed NULL. Missing fields are unchanged. RESET drops + an override and restores the provider default from the mutation's snapshot. + Expressions are not encoded as strings masquerading as literal values. + + Attributes: + set_values: Optional one-row batch containing only options to set. + reset_names: Options whose stored overrides should be removed. + """ + + set_values: Annotated[TypedValueBatch | None, ArrowType(pa.binary())] = None + reset_names: list[str] = field(default_factory=list) + + def __post_init__(self) -> None: + """Reject contradictory or malformed operations before applying values.""" + _validate_names(self.reset_names, "reset option") + if self.set_values is not None: + overlap = set(self.set_values.schema.names) & set(self.reset_names) + if overlap: + raise ValueError(f"options cannot be both SET and RESET: {sorted(overlap)}") + + def apply_to( + self, + current: OptionState, + *, + defaults: TypedValueBatch, + mutable_names: Collection[str] | None = None, + ) -> OptionState: + """Validate the whole change and return a new state without mutation. + + ``defaults`` declares all accepted fields, their exact Arrow types, + nullability and metadata, and current reset values. It must be resolved + in the same transaction as this mutation. SET fields must match their + declared fields exactly; no cast, case conversion or metadata loss is + allowed. ``mutable_names`` may further restrict which options change. + + A provider validates any additional value/dependency constraints before + persisting the returned state. Returning a candidate does not advertise + durable atomicity, rollback, replay safety, or engine DDL support. + """ + # Frozen dataclasses contain mutable lists: validate again at use time. + self.__post_init__() + current.__post_init__() + current.values.validate_schema(defaults.schema) + names = defaults.schema.names + sets = set(self.set_values.schema.names) if self.set_values is not None else set() + resets = set(self.reset_names) + changed = sets | resets + unknown = changed - set(names) + if unknown: + raise ValueError(f"unknown option names: {sorted(unknown)}") + if mutable_names is not None: + forbidden = changed - set(mutable_names) + if forbidden: + raise ValueError(f"options are not mutable: {sorted(forbidden)}") + if self.set_values is not None: + expected = pa.schema( + [defaults.schema.field(name) for name in self.set_values.schema.names], + ).with_metadata(defaults.schema.metadata) + self.set_values.validate_schema(expected) + arrays = [] + for name in names: + if name in sets: + assert self.set_values is not None + arrays.append(self.set_values.batch.column(name)) + elif name in resets: + arrays.append(defaults.batch.column(name)) + else: + arrays.append(current.values.batch.column(name)) + values = ( + TypedValueBatch(pa.RecordBatch.from_arrays(arrays, schema=defaults.schema)) + if names + else TypedValueBatch.empty(metadata=defaults.schema.metadata) + ) + explicit = (set(current.explicit_names) | sets) - resets + return OptionState(values=values, explicit_names=[name for name in names if name in explicit]) diff --git a/vgi/catalog/query.py b/vgi/catalog/query.py new file mode 100644 index 00000000..6723aeac --- /dev/null +++ b/vgi/catalog/query.py @@ -0,0 +1,383 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Negotiated catalog query and explicit statement preparation contracts. + +SQL encoding, semantic equivalence, and authorization are separate concerns. +These records never execute SQL. A decline is a decision made before execution; +authentication, malformed data and backend failures remain errors. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field, replace +from typing import Any, Self, cast + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.utils import IpcValidation + +from vgi.catalog.dependencies import CatalogObjectRef +from vgi.catalog.evaluation_context import QueryEvaluationContext as QueryEvaluationContext +from vgi.query_parameters import read_parameter_schema +from vgi.typed_values import TypedValueBatch + +QUERY_ENCODING = "duckdb.sql.v1" +QUERY_EXPLICIT = "vgi.remote.readonly.v1" +QUERY_AUTOMATIC = "vgi.duckdb.relational.v1" +QUERY_STATEMENT = "vgi.remote.statement.v1" +QUERY_EXECUTOR = "__vgi_catalog_query" +STATEMENT_RESULT_KEY = b"vgi.statement.result.v1" +_MODE_PROFILES = {"explicit": QUERY_EXPLICIT, "automatic": QUERY_AUTOMATIC, "statement": QUERY_STATEMENT} +MAX_QUERY_BYTES = 1024 * 1024 +MAX_QUERY_REQUEST_BYTES = 8 * 1024 * 1024 +MAX_QUERY_SCHEMA_BYTES = 1024 * 1024 +MAX_QUERY_TOKEN_BYTES = 64 * 1024 + + +def _text(value: str, label: str, *, maximum: int = 4096, empty: bool = False) -> None: + if not isinstance(value, str) or "\0" in value or (not empty and not value): + raise ValueError(f"invalid {label}") + if len(value.encode("utf-8")) > maximum: + raise ValueError(f"{label} is too large") + + +def _integer(value: object, label: str, *, minimum: int = 0, maximum: int = 2**63 - 1) -> None: + if type(value) is not int or not minimum <= value <= maximum: + raise ValueError(f"invalid {label}") + + +def _token(value: object, label: str) -> None: + if not isinstance(value, bytes) or not 0 < len(value) <= MAX_QUERY_TOKEN_BYTES: + raise ValueError(f"invalid {label}") + + +def _names(values: list[str], label: str, *, empty: bool = False) -> None: + if not isinstance(values, list) or (not empty and not values) or len(values) != len(set(values)): + raise ValueError(f"{label} must be a nonempty list of unique names") + for value in values: + _text(value, label, maximum=256) + + +def read_query_schema(payload: bytes | None) -> pa.Schema: + """Read one bounded schema message without accepting a table or trailing data.""" + if not isinstance(payload, bytes) or not 0 < len(payload) <= MAX_QUERY_SCHEMA_BYTES: + raise ValueError("invalid query schema size") + source = pa.BufferReader(payload) + result = pa.ipc.read_schema(cast(Any, source)) + if source.tell() != len(payload): + raise ValueError("query schema has trailing data") + return result + + +def statement_result_kind(schema: pa.Schema) -> str: + """Validate a statement's fixed result contract before execution. + + A command produces exactly one completion row: nullable int64 affected_rows + (NULL for DDL). RETURNING produces rows of its declared schema, possibly none. + Stream EOS establishes statement completion, never transaction commit. + """ + if not schema.equals(schema.with_metadata(schema.metadata), check_metadata=True): + raise ValueError("statement output schema metadata keys must be unique") + kind = (schema.metadata or {}).get(STATEMENT_RESULT_KEY) + if kind == b"command": + expected = pa.schema([pa.field("affected_rows", pa.int64())]) + if not schema.remove_metadata().equals(expected, check_metadata=True): + raise ValueError("statement command requires nullable int64 affected_rows") + elif kind == b"rows": + if not len(schema): + raise ValueError("statement rows require a nonempty output schema") + else: + raise ValueError("statement output requires command or rows result metadata") + return kind.decode("ascii") + + +class _QueryRecord(ArrowSerializableDataclass): + @classmethod + def deserialize_from_bytes(cls, data: bytes, ipc_validation: IpcValidation = IpcValidation.FULL) -> Self: + """Bound the envelope before nested decoding and require one complete row.""" + if not isinstance(data, bytes) or not 0 < len(data) <= MAX_QUERY_REQUEST_BYTES: + raise ValueError("invalid query record size") + return cls.deserialize_from_batch(TypedValueBatch.deserialize_from_bytes(data).batch) + + @classmethod + def deserialize_from_batch( + cls, + batch: pa.RecordBatch, + custom_metadata: pa.KeyValueMetadata | None = None, + *, + ipc_validation: IpcValidation = IpcValidation.FULL, + ) -> Self: + """Validate full Arrow buffers, required fields and exact field definitions.""" + if batch.num_rows != 1 or not batch.schema.equals(cls.ARROW_SCHEMA, check_metadata=False): + raise ValueError(f"{cls.__name__} requires one row with its complete schema") + if batch.nbytes > MAX_QUERY_REQUEST_BYTES: + raise ValueError("query record is too large") + TypedValueBatch(batch) # Includes required nested fields, not just Arrow buffer validity. + return super().deserialize_from_batch(batch, custom_metadata, ipc_validation=IpcValidation.FULL) + + def serialize_to_bytes(self) -> bytes: + """Enforce the same envelope bound for locally constructed requests.""" + if validate := getattr(self, "__post_init__", None): + validate() + result = super().serialize_to_bytes() + if len(result) > MAX_QUERY_REQUEST_BYTES: + raise ValueError("query record is too large") + return result + + +@dataclass(frozen=True, kw_only=True) +class ClientQueryCapabilities(_QueryRecord): + """An offer, including the actual engine build; never an authorization grant.""" + + encodings: list[str] + semantic_profiles: list[str] + engine_version: str + max_query_bytes: int = MAX_QUERY_BYTES + + def __post_init__(self) -> None: + """Validate offers while allowing unknown names to be ignored in selection.""" + _names(self.encodings, "query encodings", empty=True) + _names(self.semantic_profiles, "query profiles", empty=True) + _text(self.engine_version, "client engine version") + _integer(self.max_query_bytes, "maximum SQL size", minimum=1, maximum=MAX_QUERY_BYTES) + + +@dataclass(frozen=True, kw_only=True) +class QueryCapabilities(_QueryRecord): + """An attachment's immutable query agreement, including provider policy revision. + + Providers select automatic support only for certified client engine builds. + The generic intersection helper cannot establish semantic equivalence. + """ + + encoding: str + semantic_profiles: list[str] + execution_modes: list[str] + backend_engine: str + backend_version: str + capability_revision: str + default_evaluation_context: QueryEvaluationContext + max_query_bytes: int = MAX_QUERY_BYTES + + def __post_init__(self) -> None: + """Require internally consistent known execution modes and profiles.""" + if self.encoding != QUERY_ENCODING: + raise ValueError("unknown selected query encoding") + _names(self.semantic_profiles, "selected query profiles") + _names(self.execution_modes, "query execution modes") + if not set(self.semantic_profiles) <= set(_MODE_PROFILES.values()): + raise ValueError("unknown selected query profile") + if set(self.execution_modes) != { + mode for mode, profile in _MODE_PROFILES.items() if profile in self.semantic_profiles + }: + raise ValueError("query modes must match selected semantic profiles") + for label in ["backend_engine", "backend_version", "capability_revision"]: + _text(getattr(self, label), label) + if not isinstance(self.default_evaluation_context, QueryEvaluationContext): + raise ValueError("query capabilities require a typed evaluation context") + self.default_evaluation_context.__post_init__() + _integer(self.max_query_bytes, "maximum SQL size", minimum=1, maximum=MAX_QUERY_BYTES) + + def select(self, offer: ClientQueryCapabilities | None, *, supports_transactions: bool) -> QueryCapabilities | None: + """Intersect understood offers; never infer automatic support from an engine name.""" + self.__post_init__() + if type(supports_transactions) is not bool: + raise ValueError("transaction support must be a boolean") + if offer is None: + return None + offer.__post_init__() + if self.encoding not in offer.encodings: + return None + profiles = set(self.semantic_profiles) & set(offer.semantic_profiles) + if not supports_transactions: + profiles.discard(QUERY_AUTOMATIC) + profiles.discard(QUERY_STATEMENT) + if not profiles: + return None + return replace( + self, + semantic_profiles=sorted(profiles), + execution_modes=sorted(mode for mode, profile in _MODE_PROFILES.items() if profile in profiles), + max_query_bytes=min(self.max_query_bytes, offer.max_query_bytes), + ) + + +@dataclass(frozen=True, kw_only=True) +class QueryObjectReference(_QueryRecord): + """One logical object and its expected schema, with no alias or engine OID.""" + + object: CatalogObjectRef + expected_schema: bytes + + def __post_init__(self) -> None: + """Retain raw nested schema paths and validate the schema-only envelope.""" + if not isinstance(self.object, CatalogObjectRef): + raise ValueError("query object reference requires a CatalogObjectRef") + self.object.__post_init__() + if len(self.object.schema_path) > 32: + raise ValueError("query object schema path is too deep") + read_query_schema(self.expected_schema) + + +@dataclass(frozen=True, kw_only=True) +class CatalogQueryPrepareRequest(_QueryRecord): + """Bind a read query and parameter schema without evaluating its result.""" + + attach_opaque_data: bytes + encoding: str + semantic_profile: str + mode: str + capability_revision: str + query: str + parameter_schema: bytes + evaluation_context: QueryEvaluationContext + transaction_opaque_data: bytes | None = None + catalog_version: int | None = None + catalog_etag: str | None = None + referenced_objects: list[QueryObjectReference] = field(default_factory=list) + expected_output_schema: bytes | None = None + required_ordering: str = "unspecified" + + def __post_init__(self) -> None: + """Validate the mode-specific contract before provider parsing or binding.""" + _token(self.attach_opaque_data, "query attachment") + if self.transaction_opaque_data is not None: + _token(self.transaction_opaque_data, "query transaction") + if self.encoding != QUERY_ENCODING: + raise ValueError("unknown selected query encoding") + if _MODE_PROFILES.get(self.mode) != self.semantic_profile: + raise ValueError("query mode and semantic profile disagree") + _text(self.capability_revision, "query capability revision") + _text(self.query, "SQL query", maximum=MAX_QUERY_BYTES) + read_parameter_schema(self.parameter_schema) + if not isinstance(self.evaluation_context, QueryEvaluationContext): + raise ValueError("query requires a typed evaluation context") + self.evaluation_context.__post_init__() + if self.required_ordering not in {"unspecified", "query_order"}: + raise ValueError("unknown required result ordering") + if self.catalog_version is not None: + _integer(self.catalog_version, "catalog metadata version") + if self.catalog_etag is not None: + _text(self.catalog_etag, "catalog etag", empty=True) + if not isinstance(self.referenced_objects, list) or len(self.referenced_objects) > 1024: + raise ValueError("invalid query object manifest") + seen = set() + for reference in self.referenced_objects: + if not isinstance(reference, QueryObjectReference): + raise ValueError("query manifest requires typed object references") + reference.__post_init__() + key = reference.object.key() + if key in seen: + raise ValueError("duplicate query object reference") + seen.add(key) + if self.expected_output_schema is not None: + read_query_schema(self.expected_output_schema) + if self.mode == "automatic": + if ( + self.transaction_opaque_data is None + or self.catalog_version is None + or self.expected_output_schema is None + ): + raise ValueError("automatic query requires a transaction, metadata version and output contract") + if not self.referenced_objects or any(r.object.kind != "table" for r in self.referenced_objects): + raise ValueError("automatic v1 requires a nonempty table-only object manifest") + elif self.referenced_objects: + raise ValueError("explicit query resolves its own objects and requires an empty manifest") + if self.mode == "statement" and self.transaction_opaque_data is None: + raise ValueError("statement execution requires a transaction") + + def validate_agreement(self, selected: QueryCapabilities) -> None: + """Require the immutable encoding, mode, context and policy revision.""" + self.__post_init__() + selected.__post_init__() + if ( + self.encoding != selected.encoding + or self.semantic_profile not in selected.semantic_profiles + or self.mode not in selected.execution_modes + or self.capability_revision != selected.capability_revision + ): + raise ValueError("query preparation does not match the attachment agreement") + if len(self.query.encode("utf-8")) > selected.max_query_bytes: + raise ValueError("query exceeds the selected SQL size limit") + if self.mode in {"explicit", "statement"} and self.evaluation_context != selected.default_evaluation_context: + raise ValueError("explicit query must use the attachment's provider evaluation context") + + +@dataclass(frozen=True, kw_only=True) +class CatalogQueryPrepareResult(_QueryRecord): + """An exclusive accepted/declined union; decline never masks an execution error.""" + + status: str + reason_code: str | None = None + reason_detail: str | None = None + prepared_query_token: bytes | None = None + output_schema: bytes | None = None + result_ordering: str | None = None + validated_catalog_version: int | None = None + validated_catalog_etag: str | None = None + context_digest: bytes | None = None + expires_at_epoch_ms: int | None = None + estimated_rows: int | None = None + estimated_bytes: int | None = None + + def __post_init__(self) -> None: + """Reject incomplete, contradictory and unknown status combinations.""" + if self.status == "declined": + if not isinstance(self.reason_code, str) or not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", self.reason_code): + raise ValueError("declined query requires a bounded reason code") + if self.reason_detail is not None: + _text(self.reason_detail, "query decline detail", empty=True) + accepted = [ + self.prepared_query_token, + self.output_schema, + self.result_ordering, + self.validated_catalog_version, + self.validated_catalog_etag, + self.context_digest, + self.expires_at_epoch_ms, + self.estimated_rows, + self.estimated_bytes, + ] + if any(value is not None for value in accepted): + raise ValueError("declined query cannot carry acceptance fields") + return + if self.status != "accepted": + raise ValueError("unknown query preparation status") + if self.reason_code is not None or self.reason_detail is not None: + raise ValueError("accepted query cannot carry decline reasons") + _token(self.prepared_query_token, "prepared query token") + read_query_schema(self.output_schema) + if self.result_ordering not in {"unspecified", "query_order"}: + raise ValueError("accepted query requires a known result ordering") + if not isinstance(self.context_digest, bytes) or len(self.context_digest) != 32: + raise ValueError("accepted query requires a 32-byte context digest") + _integer(self.expires_at_epoch_ms, "prepared query expiry", minimum=1) + for name in ["validated_catalog_version", "estimated_rows", "estimated_bytes"]: + value = getattr(self, name) + if value is not None: + _integer(value, name) + if self.validated_catalog_etag is not None: + _text(self.validated_catalog_etag, "validated catalog etag", empty=True) + + def validate_request(self, request: CatalogQueryPrepareRequest) -> None: + """Check context, exact output schema and order before accepting a plan.""" + self.__post_init__() + request.__post_init__() + if self.status == "declined": + return + if self.context_digest != request.evaluation_context.digest(request.encoding, request.semantic_profile): + raise ValueError("query preparation context digest mismatch") + if request.required_ordering == "query_order" and self.result_ordering != "query_order": + raise ValueError("query preparation lost required ordering") + if request.mode == "automatic" and self.validated_catalog_version is None: + raise ValueError("accepted automatic query requires a validated metadata version") + if request.mode == "statement": + statement_result_kind(read_query_schema(self.output_schema)) + elif STATEMENT_RESULT_KEY in (read_query_schema(self.output_schema).metadata or {}): + raise ValueError("statement result metadata requires the statement profile") + if request.expected_output_schema is not None: + actual = read_query_schema(self.output_schema) + expected = read_query_schema(request.expected_output_schema) + if not actual.equals(expected, check_metadata=True): + raise ValueError("query preparation output contract mismatch") diff --git a/vgi/catalog/snapshot.py b/vgi/catalog/snapshot.py index a6705aa2..444472e3 100644 --- a/vgi/catalog/snapshot.py +++ b/vgi/catalog/snapshot.py @@ -62,6 +62,7 @@ class SnapshotRequest: attach_opaque_data: bytes = field(repr=False) attachment_scope: str catalog_version: int + capability_projection: str = "" @dataclass(frozen=True, kw_only=True) @@ -430,6 +431,7 @@ def cache_key(self, request: SnapshotRequest) -> str: identity.source_revision, identity.visibility_scope if identity.visibility_scope is not None else request.attachment_scope, str(identity.catalog_version), + request.capability_projection, self.repository.profile, compression, ] @@ -606,7 +608,7 @@ def resolve( self, request: SnapshotRequest, build: Callable[[], CatalogContentsResponse], if_none_match: str | None = None ) -> CatalogContentsResponse | ExternalRef: """Resolve after authorization; warm hits perform no object GET or serialization.""" - if self.policy.validator is not None and if_none_match is not None: + if self.policy.validator is not None and if_none_match is not None and not request.capability_projection: etag = self.policy.validator(request) if etag is not None and etag == if_none_match: return CatalogContentsResponse(catalog_version=request.catalog_version, etag=etag, not_modified=True) diff --git a/vgi/catalog/sql.py b/vgi/catalog/sql.py new file mode 100644 index 00000000..8218562c --- /dev/null +++ b/vgi/catalog/sql.py @@ -0,0 +1,165 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Explicit SQL definition carriers, independent of engine-private syntax trees. + +These codecs validate representation, not SQL semantics. The receiving owner +must bind the selected profile in the enclosing operation's declared scope. +""" + +from __future__ import annotations + +import base64 +import binascii +import re +from dataclasses import dataclass +from typing import Any, Literal, cast + +import pyarrow as pa + +from vgi.catalog.evaluation_context import MAX_SQL_RECORD_BYTES, QueryEvaluationContext, _SqlRecord, _text +from vgi.schema_path import SchemaPath, schema_path_key + +DEFAULT_EXPRESSION_KEY = b"vgi.column.default.v1" +GENERATED_EXPRESSION_KEY = b"vgi.column.generated.v1" +GENERATION_KEY = b"vgi.column.generation.v1" +_EXPRESSION_KEYS = {DEFAULT_EXPRESSION_KEY, GENERATED_EXPRESSION_KEY, GENERATION_KEY} + + +def _definition(sql: str, profile: str, schema_path: SchemaPath, context: QueryEvaluationContext) -> None: + _text(sql, "SQL definition", maximum=1024 * 1024) + if not sql.strip(): + raise ValueError("SQL definition must be nonempty") + if not isinstance(profile, str) or not re.fullmatch( + r"[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*\.v[1-9][0-9]*", profile + ): + raise ValueError("SQL profile must be a versioned semantic name") + _text(profile, "SQL profile", maximum=256) + schema_path_key(schema_path) + if len(schema_path) > 32: + raise ValueError("SQL owner schema is too deep") + for component in schema_path: + _text(component, "SQL owner identifier") + if not isinstance(context, QueryEvaluationContext): + raise TypeError("SQL definition requires an explicit evaluation context") + context.__post_init__() + + +@dataclass(frozen=True, kw_only=True) +class SqlExpression(_SqlRecord): + """One scalar expression, interpreted only by a qualified owning binder.""" + + sql: str + profile: str + schema_path: SchemaPath + evaluation_context: QueryEvaluationContext + + def __post_init__(self) -> None: + """Validate the bounded carrier and required evaluation context.""" + _definition(self.sql, self.profile, self.schema_path, self.evaluation_context) + + def validate_owner(self, schema_path: SchemaPath) -> None: + """Require the expression's raw owner path to match its operation.""" + self.__post_init__() + if schema_path_key(self.schema_path) != schema_path_key(schema_path): + raise ValueError("SQL expression owner schema does not match the target schema") + + +@dataclass(frozen=True, kw_only=True) +class SqlStatement(_SqlRecord): + """One profiled query or DML statement; the codec grants no execution rights.""" + + sql: str + profile: str + schema_path: SchemaPath + evaluation_context: QueryEvaluationContext + + def __post_init__(self) -> None: + """Validate the carrier; a receiving binder must reject scripts/control SQL.""" + _definition(self.sql, self.profile, self.schema_path, self.evaluation_context) + + +@dataclass(frozen=True, kw_only=True) +class ColumnExpressions: + """Decoded authoritative default or generated metadata for one Arrow field.""" + + default: SqlExpression | None = None + generated: SqlExpression | None = None + generation: Literal["virtual", "stored"] | None = None + + def __post_init__(self) -> None: + """Reject mixed defaults and inconsistent generated categories.""" + for expression in (self.default, self.generated): + if expression is not None: + if not isinstance(expression, SqlExpression): + raise TypeError("column expressions require SqlExpression records") + expression.__post_init__() + if self.default is not None and self.generated is not None: + raise ValueError("a generated column cannot also have a default") + if (self.generated is None) != (self.generation is None): + raise ValueError("generated category and expression must occur together") + if self.generation is not None and self.generation not in {"virtual", "stored"}: + raise ValueError("unknown generated column category") + + +def _metadata(field: pa.Field[Any]) -> dict[bytes, bytes]: + if not field.equals(field.with_metadata(cast(Any, field.metadata)), check_metadata=True): + raise ValueError("column expression metadata keys must be unique") + metadata = field.metadata or {} + for key in metadata: + if ( + key.startswith((b"vgi.column.default.", b"vgi.column.generated.", b"vgi.column.generation.")) + and key not in _EXPRESSION_KEYS + ): + raise ValueError("unknown column expression metadata version") + return metadata + + +def _decode_expression_metadata(value: bytes) -> SqlExpression: + # Arrow metadata is a UTF-8 string in the format and in several SDKs. + # Check the encoded bound before allocating the decoded IPC envelope. + maximum = 4 * ((MAX_SQL_RECORD_BYTES + 2) // 3) + if not 0 < len(value) <= maximum: + raise ValueError("invalid column expression metadata size") + try: + payload = base64.b64decode(value, validate=True) + except binascii.Error as error: + raise ValueError("column expression metadata requires canonical padded base64") from error + if not 0 < len(payload) <= MAX_SQL_RECORD_BYTES: + raise ValueError("invalid column expression metadata size") + if base64.b64encode(payload) != value: + raise ValueError("column expression metadata requires canonical padded base64") + return SqlExpression.deserialize_from_bytes(payload) + + +def column_expressions(field: pa.Field[Any]) -> ColumnExpressions: + """Decode canonical padded base64 IPC metadata; legacy text is not a record.""" + metadata = _metadata(field) + if _EXPRESSION_KEYS & metadata.keys() and {b"default", b"generated_expression"} & metadata.keys(): + raise ValueError("canonical column expressions conflict with legacy metadata") + default = metadata.get(DEFAULT_EXPRESSION_KEY) + generated = metadata.get(GENERATED_EXPRESSION_KEY) + generation = metadata.get(GENERATION_KEY) + if generation not in {None, b"virtual", b"stored"}: + raise ValueError("unknown generated column category") + return ColumnExpressions( + default=None if default is None else _decode_expression_metadata(default), + generated=None if generated is None else _decode_expression_metadata(generated), + generation=None if generation is None else cast(Literal["virtual", "stored"], generation.decode()), + ) + + +def with_column_expressions(field: pa.Field[Any], expressions: ColumnExpressions) -> pa.Field[Any]: + """Replace canonical expression metadata while preserving unrelated bytes.""" + expressions.__post_init__() + column_expressions(field) + metadata = _metadata(field) + if {b"default", b"generated_expression"} & metadata.keys(): + raise ValueError("legacy expression metadata requires explicit translation") + for key in _EXPRESSION_KEYS: + metadata.pop(key, None) + if expressions.default is not None: + metadata[DEFAULT_EXPRESSION_KEY] = base64.b64encode(expressions.default.serialize_to_bytes()) + if expressions.generated is not None: + metadata[GENERATED_EXPRESSION_KEY] = base64.b64encode(expressions.generated.serialize_to_bytes()) + metadata[GENERATION_KEY] = str(expressions.generation).encode() + return field.with_metadata(cast(Any, metadata)) diff --git a/vgi/client/catalog_mixin.py b/vgi/client/catalog_mixin.py index 3c061efa..fe32b05e 100644 --- a/vgi/client/catalog_mixin.py +++ b/vgi/client/catalog_mixin.py @@ -59,6 +59,7 @@ class CatalogEnabledClient(CatalogClientMixin, Client): FunctionInfo, IndexInfo, MacroInfo, + MacroOverload, MacroType, OnConflict, ScanBranch, @@ -74,14 +75,24 @@ class CatalogEnabledClient(CatalogClientMixin, Client): ViewInfo, ) from vgi.catalog.catalog_interface import deserialize_column_statistics +from vgi.catalog.constraints import ConstraintsApplyRequest, ConstraintSnapshot from vgi.catalog.contents_codec import decode_inventory +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot +from vgi.catalog.indexes import IndexDefinition +from vgi.catalog.layout import TableLayout, TableLayoutChange +from vgi.catalog.metadata_changes import CatalogMetadataChanges +from vgi.catalog.options import OptionChanges, OptionDefinitions +from vgi.catalog.query import CatalogQueryPrepareRequest, CatalogQueryPrepareResult from vgi.protocol import ( CatalogAttachRequest, CatalogCreateRequest, + ClientCapabilities, + IndexCreateRequest, MacroCreateRequest, TableCreateRequest, VgiProtocol, ) +from vgi.typed_values import TypedValueBatch # Module-level worker pool shared across all CatalogClientMixin instances. # Workers are cached by command and reused across catalog calls, avoiding @@ -350,6 +361,7 @@ def catalog_attach( options: dict[str, Any] | None = None, data_version_spec: str | None, implementation_version: str | None, + client_capabilities: ClientCapabilities | None = None, ) -> CatalogAttachResult: """Attach to a catalog. @@ -360,6 +372,9 @@ def catalog_attach( (``None`` = unconstrained — worker picks). implementation_version: Semver constraint for the worker's implementation version (``None`` = unconstrained). + client_capabilities: Capabilities of the consuming engine. ``None`` + leaves support unspecified; the worker must not infer support + for optional semantics from the Python SDK version. Returns: [`CatalogAttachResult`][] with attach_opaque_data, catalog capabilities, and @@ -373,6 +388,7 @@ def catalog_attach( options=self._options_to_batch(options), data_version_spec=data_version_spec, implementation_version=implementation_version, + client_capabilities=client_capabilities, ) ) @@ -439,6 +455,34 @@ def catalog_version( transaction_opaque_data=transaction_opaque_data, ).version + def catalog_dependencies( + self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None = None + ) -> DependencySnapshot: + """Read a complete transaction-visible graph under its selected read profile.""" + with self._catalog_connect() as proxy: + return proxy.catalog_dependencies( + attach_opaque_data=attach_opaque_data, transaction_opaque_data=transaction_opaque_data + ) + + def catalog_query_prepare(self, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + """Authorize and describe a query; this does not execute or fetch rows.""" + request.__post_init__() + with self._catalog_connect() as proxy: + result = proxy.catalog_query_prepare(request=request) + result.validate_request(request) + return result + + def catalog_query_release( + self, *, attach_opaque_data: bytes, prepared_query_token: bytes, transaction_opaque_data: bytes | None = None + ) -> None: + """Release preparation resources without canceling executions already started.""" + with self._catalog_connect() as proxy: + proxy.catalog_query_release( + attach_opaque_data=attach_opaque_data, + prepared_query_token=prepared_query_token, + transaction_opaque_data=transaction_opaque_data, + ) + # ========== Transaction Methods ========== def catalog_transaction_begin(self, *, attach_opaque_data: AttachOpaqueData) -> TransactionOpaqueData | None: @@ -674,8 +718,10 @@ def schema_create( attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None = None, path: list[str], + on_conflict: OnConflict = OnConflict.ERROR, comment: str | None = None, tags: dict[str, str] | None = None, + options: TypedValueBatch | None = None, ) -> None: """Create a new schema. @@ -683,17 +729,73 @@ def schema_create( attach_opaque_data: The attachment ID from catalog_attach. transaction_opaque_data: Optional transaction ID. path: Raw schema identifier components for the new schema. + on_conflict: ERROR, IGNORE, or REPLACE for an existing schema. comment: Optional description of the schema. tags: Optional key-value tags for the schema. + options: Typed literal overrides; requires negotiated schema-option creation. """ with self._catalog_connect() as proxy: proxy.catalog_schema_create( attach_opaque_data=attach_opaque_data, path=path, + on_conflict=on_conflict, comment=comment, tags=tags, transaction_opaque_data=transaction_opaque_data, + options=options.serialize_to_bytes() if options is not None else None, + ) + + def schema_option_definitions( + self, + *, + attach_opaque_data: AttachOpaqueData, + path: list[str], + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> OptionDefinitions: + """Discover typed options and defaults for an existing or proposed schema.""" + with self._catalog_connect() as proxy: + return proxy.catalog_schema_option_definitions( + attach_opaque_data=attach_opaque_data, + path=path, + transaction_opaque_data=transaction_opaque_data, + ) + + def schema_alter_metadata( + self, + *, + attach_opaque_data: AttachOpaqueData, + path: list[str], + changes: CatalogMetadataChanges, + ignore_not_found: bool = False, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> None: + """Atomically change comments/tags; requires vgi.schema.metadata.alter.v1.""" + changes.__post_init__() + with self._catalog_connect() as proxy: + proxy.catalog_schema_alter_metadata( + attach_opaque_data=attach_opaque_data, + path=path, + changes=changes.serialize_to_bytes(), + ignore_not_found=ignore_not_found, + transaction_opaque_data=transaction_opaque_data, + ) + + def schema_alter_options( + self, + *, + attach_opaque_data: AttachOpaqueData, + path: list[str], + changes: OptionChanges, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> None: + """Atomically SET/RESET schema options; read effective values with schema_get.""" + with self._catalog_connect() as proxy: + proxy.catalog_schema_alter_options( + attach_opaque_data=attach_opaque_data, + path=path, + changes=changes.serialize_to_bytes(), + transaction_opaque_data=transaction_opaque_data, ) def schema_drop( @@ -969,9 +1071,11 @@ def table_create( name: str, columns: SerializedSchema, on_conflict: OnConflict = OnConflict.ERROR, - not_null_constraints: list[int] | None = None, - unique_constraints: list[list[int]] | None = None, - check_constraints: list[str] | None = None, + constraint_definitions: list[bytes] | None = None, + comment: str | None = None, + tags: dict[str, str] | None = None, + options: TypedValueBatch | None = None, + layout: TableLayout | None = None, ) -> None: """Create a new table. @@ -982,9 +1086,13 @@ def table_create( name: The name for the new table. columns: Serialized PyArrow schema for the table columns. on_conflict: Behavior if table already exists. - not_null_constraints: Column indices that must not be null. - unique_constraints: Lists of column indices for unique constraints. - check_constraints: SQL expressions for check constraints. + constraint_definitions: Canonical ConstraintDefinition IPC records. + Nonempty requests require enforced validation of all rows. + comment: Initial table comment under the metadata creation profile. + tags: Initial table tags under the metadata creation profile. + options: Typed option overrides under the table-option creation profile. + layout: Partition/sort policy under the layout creation profile. + """ with self._catalog_connect() as proxy: @@ -995,13 +1103,161 @@ def table_create( name=name, columns=columns, on_conflict=on_conflict, - not_null_constraints=not_null_constraints or [], - unique_constraints=unique_constraints or [], - check_constraints=check_constraints or [], + constraint_definitions=[] if constraint_definitions is None else constraint_definitions, + transaction_opaque_data=transaction_opaque_data, + comment=comment, + tags=tags, + options=options, + layout=layout, + ) + ) + + def table_constraints_apply(self, request: ConstraintsApplyRequest) -> ConstraintSnapshot: + """Apply one atomic constraint change set without retrying ambiguous replies.""" + request.__post_init__() + with self._catalog_connect() as proxy: + result = proxy.catalog_table_constraints_apply(request=request) + result.__post_init__() + if result.table != request.table: + raise ValueError("constraint mutation returned a different table") + return result + + def index_get( + self, + *, + attach_opaque_data: AttachOpaqueData, + schema_path: list[str], + name: str, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> IndexInfo | None: + """Read one resolved typed index definition.""" + with self._catalog_connect() as proxy: + infos = proxy.catalog_index_get( + attach_opaque_data=attach_opaque_data, + schema_path=schema_path, + name=name, + transaction_opaque_data=transaction_opaque_data, + ).to_infos() + if len(infos) > 1: + raise ValueError("index lookup returned more than one definition") + return infos[0] if infos else None + + def index_option_definitions( + self, + *, + attach_opaque_data: AttachOpaqueData, + table: CatalogObjectRef, + algorithm: str, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> OptionDefinitions: + """Discover typed options for an exact provider algorithm and table.""" + with self._catalog_connect() as proxy: + return proxy.catalog_index_option_definitions( + attach_opaque_data=attach_opaque_data, + table=table.serialize_to_bytes(), + algorithm=algorithm, + transaction_opaque_data=transaction_opaque_data, + ) + + def index_create( + self, + *, + attach_opaque_data: AttachOpaqueData, + schema_path: list[str], + name: str, + definition: IndexDefinition, + on_conflict: OnConflict = OnConflict.ERROR, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> None: + """Create one complete typed index without retrying an ambiguous mutation.""" + with self._catalog_connect() as proxy: + proxy.catalog_index_create( + request=IndexCreateRequest( + attach_opaque_data=attach_opaque_data, + schema_path=schema_path, + name=name, + definition=definition, + on_conflict=on_conflict, transaction_opaque_data=transaction_opaque_data, ) ) + def index_drop( + self, + *, + attach_opaque_data: AttachOpaqueData, + schema_path: list[str], + name: str, + ignore_not_found: bool = False, + cascade: bool = False, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> None: + """Drop an index under the provider's atomic dependency-closure checks.""" + with self._catalog_connect() as proxy: + proxy.catalog_index_drop( + attach_opaque_data=attach_opaque_data, + schema_path=schema_path, + name=name, + ignore_not_found=ignore_not_found, + cascade=cascade, + transaction_opaque_data=transaction_opaque_data, + ) + + def table_option_definitions( + self, + *, + attach_opaque_data: AttachOpaqueData, + schema_path: list[str], + name: str, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> OptionDefinitions: + """Discover typed table-option definitions in the current snapshot.""" + with self._catalog_connect() as proxy: + return proxy.catalog_table_option_definitions( + attach_opaque_data=attach_opaque_data, + schema_path=schema_path, + name=name, + transaction_opaque_data=transaction_opaque_data, + ) + + def table_alter_options( + self, + *, + attach_opaque_data: AttachOpaqueData, + schema_path: list[str], + name: str, + changes: OptionChanges, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> None: + """Apply one atomic SET/RESET operation to table options.""" + with self._catalog_connect() as proxy: + proxy.catalog_table_alter_options( + attach_opaque_data=attach_opaque_data, + schema_path=schema_path, + name=name, + changes=changes.serialize_to_bytes(), + transaction_opaque_data=transaction_opaque_data, + ) + + def table_alter_layout( + self, + *, + attach_opaque_data: AttachOpaqueData, + schema_path: list[str], + name: str, + changes: TableLayoutChange, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> None: + """Replace layout components with an explicit existing-data policy.""" + with self._catalog_connect() as proxy: + proxy.catalog_table_alter_layout( + attach_opaque_data=attach_opaque_data, + schema_path=schema_path, + name=name, + changes=changes.serialize_to_bytes(), + transaction_opaque_data=transaction_opaque_data, + ) + def table_drop( self, *, @@ -1213,6 +1469,39 @@ def table_rename( transaction_opaque_data=transaction_opaque_data, ) + def table_column_comment_set( + self, + *, + attach_opaque_data: AttachOpaqueData, + schema_path: list[str], + name: str, + column_path: list[str], + comment: str | None = None, + ignore_not_found: bool = False, + transaction_opaque_data: TransactionOpaqueData | None = None, + ) -> None: + """Set or remove a comment on an exact raw column path. + + Args: + attach_opaque_data: The attachment ID from catalog_attach. + schema_path: The owning schema's raw components. + name: The table name. + column_path: Raw components identifying the target field. + comment: The replacement comment, or None to clear it. + ignore_not_found: Ignore a missing table. + transaction_opaque_data: Optional transaction handle. + """ + with self._catalog_connect() as proxy: + proxy.catalog_table_column_comment_set( + attach_opaque_data=attach_opaque_data, + schema_path=schema_path, + name=name, + column_path=column_path, + comment=comment, + ignore_not_found=ignore_not_found, + transaction_opaque_data=transaction_opaque_data, + ) + def table_column_add( self, *, @@ -1220,6 +1509,7 @@ def table_column_add( transaction_opaque_data: TransactionOpaqueData | None = None, schema_path: list[str], name: str, + column_path: list[str], column_definition: SerializedSchema, ignore_not_found: bool = False, if_column_not_exists: bool = False, @@ -1231,6 +1521,7 @@ def table_column_add( transaction_opaque_data: Optional transaction ID. schema_path: The schema containing the table. name: The table name. + column_path: Raw components identifying the target field. column_definition: Serialized schema with single field for the new column. ignore_not_found: If True, don't error if table doesn't exist. if_column_not_exists: If True, don't error if column already exists. @@ -1241,6 +1532,7 @@ def table_column_add( attach_opaque_data=attach_opaque_data, schema_path=schema_path, name=name, + column_path=column_path, column_definition=column_definition, ignore_not_found=ignore_not_found, if_column_not_exists=if_column_not_exists, @@ -1254,7 +1546,7 @@ def table_column_drop( transaction_opaque_data: TransactionOpaqueData | None = None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, if_column_exists: bool = False, cascade: bool = False, @@ -1266,7 +1558,7 @@ def table_column_drop( transaction_opaque_data: Optional transaction ID. schema_path: The schema containing the table. name: The table name. - column_name: The name of the column to drop. + column_path: The name of the column to drop. ignore_not_found: If True, don't error if table doesn't exist. if_column_exists: If True, don't error if column doesn't exist. cascade: If True, drop dependent constraints. @@ -1277,7 +1569,7 @@ def table_column_drop( attach_opaque_data=attach_opaque_data, schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, ignore_not_found=ignore_not_found, if_column_exists=if_column_exists, cascade=cascade, @@ -1291,7 +1583,7 @@ def table_column_rename( transaction_opaque_data: TransactionOpaqueData | None = None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], new_column_name: str, ignore_not_found: bool = False, ) -> None: @@ -1302,7 +1594,7 @@ def table_column_rename( transaction_opaque_data: Optional transaction ID. schema_path: The schema containing the table. name: The table name. - column_name: The current name of the column. + column_path: The current name of the column. new_column_name: The new name for the column. ignore_not_found: If True, don't error if table doesn't exist. @@ -1312,7 +1604,7 @@ def table_column_rename( attach_opaque_data=attach_opaque_data, schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, new_column_name=new_column_name, ignore_not_found=ignore_not_found, transaction_opaque_data=transaction_opaque_data, @@ -1325,7 +1617,7 @@ def table_column_default_set( transaction_opaque_data: TransactionOpaqueData | None = None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], expression: SqlExpression, ignore_not_found: bool = False, ) -> None: @@ -1336,18 +1628,21 @@ def table_column_default_set( transaction_opaque_data: Optional transaction ID. schema_path: The schema containing the table. name: The table name. - column_name: The column to set the default for. + column_path: The column to set the default for. expression: The SQL expression for the default value. ignore_not_found: If True, don't error if table doesn't exist. """ + if not isinstance(expression, SqlExpression): + raise TypeError("expression must be a SqlExpression") + expression.validate_owner(schema_path) with self._catalog_connect() as proxy: proxy.catalog_table_column_default_set( attach_opaque_data=attach_opaque_data, schema_path=schema_path, name=name, - column_name=column_name, - expression=expression, + column_path=column_path, + expression=expression.serialize_to_bytes(), ignore_not_found=ignore_not_found, transaction_opaque_data=transaction_opaque_data, ) @@ -1359,7 +1654,7 @@ def table_column_default_drop( transaction_opaque_data: TransactionOpaqueData | None = None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, ) -> None: """Remove the default value from a column. @@ -1369,7 +1664,7 @@ def table_column_default_drop( transaction_opaque_data: Optional transaction ID. schema_path: The schema containing the table. name: The table name. - column_name: The column to remove the default from. + column_path: The column to remove the default from. ignore_not_found: If True, don't error if table doesn't exist. """ @@ -1378,7 +1673,7 @@ def table_column_default_drop( attach_opaque_data=attach_opaque_data, schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, ignore_not_found=ignore_not_found, transaction_opaque_data=transaction_opaque_data, ) @@ -1390,6 +1685,7 @@ def table_column_type_change( transaction_opaque_data: TransactionOpaqueData | None = None, schema_path: list[str], name: str, + column_path: list[str], column_definition: SerializedSchema, expression: SqlExpression | None = None, ignore_not_found: bool = False, @@ -1401,19 +1697,25 @@ def table_column_type_change( transaction_opaque_data: Optional transaction ID. schema_path: The schema containing the table. name: The table name. + column_path: Raw components identifying the target field. column_definition: Serialized schema with single field defining the new type. Column name is taken from the schema field name. expression: Optional SQL expression to convert existing values. ignore_not_found: If True, don't error if table doesn't exist. """ + if expression is not None: + if not isinstance(expression, SqlExpression): + raise TypeError("expression must be a SqlExpression or None") + expression.validate_owner(schema_path) with self._catalog_connect() as proxy: proxy.catalog_table_column_type_change( attach_opaque_data=attach_opaque_data, schema_path=schema_path, name=name, + column_path=column_path, column_definition=column_definition, - expression=expression, + expression=None if expression is None else expression.serialize_to_bytes(), ignore_not_found=ignore_not_found, transaction_opaque_data=transaction_opaque_data, ) @@ -1425,7 +1727,7 @@ def table_not_null_drop( transaction_opaque_data: TransactionOpaqueData | None = None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, ) -> None: """Remove NOT NULL constraint from a column. @@ -1435,7 +1737,7 @@ def table_not_null_drop( transaction_opaque_data: Optional transaction ID. schema_path: The schema containing the table. name: The table name. - column_name: The column to remove NOT NULL from. + column_path: The column to remove NOT NULL from. ignore_not_found: If True, don't error if table doesn't exist. """ @@ -1444,7 +1746,7 @@ def table_not_null_drop( attach_opaque_data=attach_opaque_data, schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, ignore_not_found=ignore_not_found, transaction_opaque_data=transaction_opaque_data, ) @@ -1456,7 +1758,7 @@ def table_not_null_set( transaction_opaque_data: TransactionOpaqueData | None = None, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, ) -> None: """Add NOT NULL constraint to a column. @@ -1466,7 +1768,7 @@ def table_not_null_set( transaction_opaque_data: Optional transaction ID. schema_path: The schema containing the table. name: The table name. - column_name: The column to add NOT NULL to. + column_path: The column to add NOT NULL to. ignore_not_found: If True, don't error if table doesn't exist. """ @@ -1475,7 +1777,7 @@ def table_not_null_set( attach_opaque_data=attach_opaque_data, schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, ignore_not_found=ignore_not_found, transaction_opaque_data=transaction_opaque_data, ) @@ -1519,6 +1821,11 @@ def view_create( name: str, definition: str, on_conflict: OnConflict = OnConflict.ERROR, + aliases: list[str] | None = None, + security: str = "regular", + comment: str | None = None, + tags: dict[str, str] | None = None, + column_comments: dict[str, str] | None = None, ) -> None: """Create a new view. @@ -1529,6 +1836,11 @@ def view_create( name: The name for the new view. definition: The SQL SELECT statement defining the view. on_conflict: Behavior if view already exists. + aliases: Ordered raw output identifiers; omitted uses query names. + security: Optimization barrier mode; barrier requires negotiated support. + comment: Creation comment; None and an empty string remain distinct. + tags: Creation tags; omitted or empty resets tags on REPLACE. + column_comments: Comments keyed by raw output names. """ with self._catalog_connect() as proxy: @@ -1537,6 +1849,11 @@ def view_create( schema_path=schema_path, name=name, definition=definition, + aliases=aliases, + security=security, + comment=comment, + tags=tags, + column_comments=column_comments, on_conflict=on_conflict, transaction_opaque_data=transaction_opaque_data, ) @@ -1634,6 +1951,29 @@ def view_comment_set( transaction_opaque_data=transaction_opaque_data, ) + def view_column_comment_set( + self, + *, + attach_opaque_data: AttachOpaqueData, + transaction_opaque_data: TransactionOpaqueData | None = None, + schema_path: list[str], + name: str, + column_path: list[str], + comment: str | None = None, + ignore_not_found: bool = False, + ) -> None: + """Set or clear the comment keyed by a raw view output name.""" + with self._catalog_connect() as proxy: + proxy.catalog_view_column_comment_set( + attach_opaque_data=attach_opaque_data, + transaction_opaque_data=transaction_opaque_data, + schema_path=schema_path, + name=name, + column_path=column_path, + comment=comment, + ignore_not_found=ignore_not_found, + ) + # ========== Macro Methods ========== def macro_get( @@ -1672,30 +2012,10 @@ def macro_create( schema_path: list[str], name: str, macro_type: MacroType, - parameters: list[str], - definition: str, + overloads: list[MacroOverload], on_conflict: OnConflict = OnConflict.ERROR, - parameter_default_values: pa.RecordBatch | None = None, - arguments_schema: pa.Schema | None = None, ) -> None: - """Create a new macro. - - Args: - attach_opaque_data: The attachment ID from catalog_attach. - transaction_opaque_data: Optional transaction ID. - schema_path: The schema to create the macro in. - name: The name for the new macro. - macro_type: Whether this is a scalar or table macro. - parameters: Ordered list of parameter names. - definition: SQL expression (scalar) or query (table). - on_conflict: Behavior if macro already exists. - parameter_default_values: One-row `RecordBatch` with typed defaults. - arguments_schema: Optional Arrow schema (one nullable field per - parameter, in ``parameters`` order) carrying per-parameter - descriptions via the ``vgi_doc`` field metadata key. Build with - ``vgi.argument_spec.macro_arguments_schema``. - - """ + """Create or replace one complete family of explicitly scoped macro definitions.""" with self._catalog_connect() as proxy: proxy.catalog_macro_create( request=MacroCreateRequest( @@ -1703,11 +2023,8 @@ def macro_create( schema_path=schema_path, name=name, macro_type=macro_type, - parameters=parameters, - definition=definition, + overloads=overloads, on_conflict=on_conflict, - parameter_default_values=parameter_default_values, - arguments_schema=arguments_schema, transaction_opaque_data=transaction_opaque_data, ) ) diff --git a/vgi/client/cli_table.py b/vgi/client/cli_table.py index 33b3e5a2..f37b56a1 100644 --- a/vgi/client/cli_table.py +++ b/vgi/client/cli_table.py @@ -17,7 +17,11 @@ import click -from vgi.catalog import OnConflict, SerializedSchema, SqlExpression +from vgi.catalog import OnConflict, SerializedSchema +from vgi.catalog.capabilities import ROW_SCALAR_EXPRESSION +from vgi.catalog.constraints import ConstraintDefinition, ConstraintKind +from vgi.catalog.evaluation_context import QueryEvaluationContext +from vgi.catalog.sql import SqlExpression as RowExpression from vgi.client.cli_utils import ( SCHEMA_PATH, json_to_arrow_schema, @@ -127,11 +131,32 @@ def table_create( columns_json = parse_json_option(columns, "--columns") arrow_schema = json_to_arrow_schema(columns_json) - # Parse unique constraints: each is a comma-separated list of column indices - unique_constraints = [] - for u in unique: - indices = [int(i.strip()) for i in u.split(",")] - unique_constraints.append(indices) + def path(index: int) -> list[str]: + if index < 0 or index >= len(arrow_schema): + raise click.ClickException(f"Constraint column index {index} is out of range") + return [arrow_schema.field(index).name] + + definitions = [ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[path(i)]) for i in not_null] + for group in unique: + try: + indices = [int(value.strip()) for value in group.split(",")] + except ValueError as error: + raise click.ClickException("--unique requires comma-separated column indices") from error + definitions.append(ConstraintDefinition(kind=ConstraintKind.UNIQUE, column_paths=[path(i) for i in indices])) + for sql in check: + definitions.append( + ConstraintDefinition( + kind=ConstraintKind.CHECK, + check_expression=RowExpression( + sql=sql, + profile=ROW_SCALAR_EXPRESSION, + schema_path=schema_path, + evaluation_context=QueryEvaluationContext(), + ), + ) + ) + for i in not_null: + arrow_schema = arrow_schema.set(i, arrow_schema.field(i).with_nullable(False)) client.table_create( attach_opaque_data=resolved_attach_opaque_data, @@ -140,9 +165,7 @@ def table_create( name=name, columns=SerializedSchema(arrow_schema.serialize().to_pybytes()), on_conflict=OnConflict(on_conflict), - not_null_constraints=list(not_null), - unique_constraints=unique_constraints, - check_constraints=list(check), + constraint_definitions=[definition.serialize_to_bytes() for definition in definitions], ) output_json({"status": "created", "schema": schema_path, "name": name}) @@ -368,6 +391,7 @@ def column_add( transaction_opaque_data=(optional_transaction_opaque_data(transaction_opaque_data)), schema_path=schema_path, name=table_name, + column_path=[arrow_schema.field(0).name], column_definition=SerializedSchema(arrow_schema.serialize().to_pybytes()), ignore_not_found=ignore_not_found, if_column_not_exists=if_not_exists, @@ -420,7 +444,7 @@ def column_drop( transaction_opaque_data=(optional_transaction_opaque_data(transaction_opaque_data)), schema_path=schema_path, name=table_name, - column_name=column_name, + column_path=[column_name], ignore_not_found=ignore_not_found, if_column_exists=if_exists, cascade=cascade, @@ -472,7 +496,7 @@ def column_rename( transaction_opaque_data=(optional_transaction_opaque_data(transaction_opaque_data)), schema_path=schema_path, name=table_name, - column_name=column_name, + column_path=[column_name], new_column_name=new_column_name, ignore_not_found=ignore_not_found, ) @@ -515,7 +539,8 @@ def column_set_default( SCHEMA_NAME is the schema containing the table. TABLE_NAME is the table containing the column. COLUMN_NAME is the column to set the default for. - EXPRESSION is the SQL expression for the default value. + EXPRESSION is the SQL expression for the default value, evaluated in the + owning schema with the explicit UTC/binary default evaluation context. """ client, resolved_attach_opaque_data = resolve_attach(worker, attach_opaque_data, catalog_name, attach_options) @@ -524,8 +549,13 @@ def column_set_default( transaction_opaque_data=(optional_transaction_opaque_data(transaction_opaque_data)), schema_path=schema_path, name=table_name, - column_name=column_name, - expression=SqlExpression(expression), + column_path=[column_name], + expression=RowExpression( + sql=expression, + profile=ROW_SCALAR_EXPRESSION, + schema_path=schema_path, + evaluation_context=QueryEvaluationContext(), + ), ignore_not_found=ignore_not_found, ) output_json( @@ -572,7 +602,7 @@ def column_drop_default( transaction_opaque_data=(optional_transaction_opaque_data(transaction_opaque_data)), schema_path=schema_path, name=table_name, - column_name=column_name, + column_path=[column_name], ignore_not_found=ignore_not_found, ) output_json( @@ -618,7 +648,9 @@ def column_set_type( SCHEMA_NAME is the schema containing the table. TABLE_NAME is the table containing the column. - The --column option specifies the column name and new type. + The --column option specifies the column name and new type. --using is + evaluated against old rows in the owning schema with the explicit + UTC/binary default evaluation context. """ client, resolved_attach_opaque_data = resolve_attach(worker, attach_opaque_data, catalog_name, attach_options) @@ -631,8 +663,18 @@ def column_set_type( transaction_opaque_data=(optional_transaction_opaque_data(transaction_opaque_data)), schema_path=schema_path, name=table_name, + column_path=[arrow_schema.field(0).name], column_definition=SerializedSchema(arrow_schema.serialize().to_pybytes()), - expression=SqlExpression(expression) if expression else None, + expression=( + RowExpression( + sql=expression, + profile=ROW_SCALAR_EXPRESSION, + schema_path=schema_path, + evaluation_context=QueryEvaluationContext(), + ) + if expression is not None + else None + ), ignore_not_found=ignore_not_found, ) output_json( @@ -679,7 +721,7 @@ def column_set_not_null( transaction_opaque_data=(optional_transaction_opaque_data(transaction_opaque_data)), schema_path=schema_path, name=table_name, - column_name=column_name, + column_path=[column_name], ignore_not_found=ignore_not_found, ) output_json( @@ -726,7 +768,7 @@ def column_drop_not_null( transaction_opaque_data=(optional_transaction_opaque_data(transaction_opaque_data)), schema_path=schema_path, name=table_name, - column_name=column_name, + column_path=[column_name], ignore_not_found=ignore_not_found, ) output_json( diff --git a/vgi/client/cli_utils.py b/vgi/client/cli_utils.py index 8b9bab03..a1b371d8 100644 --- a/vgi/client/cli_utils.py +++ b/vgi/client/cli_utils.py @@ -12,6 +12,7 @@ from __future__ import annotations import json +from dataclasses import asdict from typing import TYPE_CHECKING, Any import click @@ -301,9 +302,8 @@ def table_info_to_dict(table_info: TableInfo) -> dict[str, Any]: "name": table_info.name, "schema_path": table_info.schema_path, "columns": arrow_schema_to_json(table_info.columns), - "not_null_constraints": table_info.not_null_constraints, - "unique_constraints": table_info.unique_constraints, - "check_constraints": table_info.check_constraints, + "constraints": [asdict(info) for info in table_info.constraint_infos()], + "definition_revision": None if table_info.definition_revision is None else table_info.definition_revision.hex(), "comment": table_info.comment, "tags": dict(table_info.tags), } @@ -323,6 +323,9 @@ def view_info_to_dict(view_info: ViewInfo) -> dict[str, Any]: "name": view_info.name, "schema_path": view_info.schema_path, "definition": view_info.definition, + "aliases": list(view_info.aliases), + "security": view_info.security, + "column_comments": dict(view_info.column_comments), "comment": view_info.comment, "tags": dict(view_info.tags), } @@ -520,7 +523,36 @@ def get_attach_opaque_data_from_options( # Auto-attach via --catalog assert catalog is not None options = attach_options or {} - result = client.catalog_attach(name=catalog, options=options, data_version_spec=None, implementation_version=None) + from vgi.catalog.capabilities import ( + CONSTRAINT_CREATE, + CONSTRAINT_ENFORCE, + CONSTRAINT_MUTATE, + CONSTRAINT_RECORDS_READ, + INDEX_DEFINITION_CREATE, + INDEX_DEFINITION_READ, + INDEX_DROP, + ROW_SCALAR_EXPRESSION, + TRANSACTION_DDL, + CatalogCapabilities, + ) + from vgi.protocol import ClientCapabilities + + offer = ClientCapabilities( + engine="vgi-cli", + native_formats=[], + catalogs=[], + can_stream=False, + filter_encodings=[], + catalog=CatalogCapabilities( + read=sorted([CONSTRAINT_RECORDS_READ, ROW_SCALAR_EXPRESSION, INDEX_DEFINITION_READ]), + ddl=sorted([CONSTRAINT_CREATE, CONSTRAINT_MUTATE, INDEX_DEFINITION_CREATE, INDEX_DROP]), + write=[CONSTRAINT_ENFORCE], + transaction=[TRANSACTION_DDL], + ), + ) + result = client.catalog_attach( + name=catalog, options=options, data_version_spec=None, implementation_version=None, client_capabilities=offer + ) # Return the attach_opaque_data and whether this is a stateful catalog # (is_stateful=True means caller should warn about using --catalog diff --git a/vgi/client/client.py b/vgi/client/client.py index 648ad8f7..a10a2669 100644 --- a/vgi/client/client.py +++ b/vgi/client/client.py @@ -115,6 +115,7 @@ ) from vgi.arguments import Arguments +from vgi.catalog.query import QUERY_EXECUTOR, CatalogQueryPrepareResult, read_query_schema from vgi.client.aggregate import AggregateClientMixin from vgi.client.catalog_mixin import CatalogClientMixin from vgi.client.errors import ClientError as ClientError # re-exported for callers @@ -140,6 +141,7 @@ VgiProtocol, _decode_parent_rows, ) +from vgi.query_parameters import QueryParameters from vgi.table_function import TableInOutFunctionInitPhase _logger = logging.getLogger("vgi.client") @@ -240,7 +242,7 @@ def _validate_accepted_max_response_bytes(value: int | None) -> int | None: #: Wire name of the primary VGI protocol, the first protocol every worker hosts. -VGI_PROTOCOL_NAME = "vgi.v2" +VGI_PROTOCOL_NAME = VgiProtocol.protocol_name @dataclass @@ -1641,7 +1643,7 @@ def list_protocols(self) -> list[HostedProtocol]: Calls ``vgi_rpc.Reflection.v1.list_protocols`` through ``vgi_rpc.list_protocols`` on the primary worker's connection, whatever - the transport, without opening a new one. ``vgi.v2`` is always first; a + the transport, without opening a new one. ``vgi.v3`` is always first; a worker's extra protocols (``Worker.hosted_protocols()``) follow, then the framework's own. Use it to discover an optional protocol before calling it, rather than calling it and reading an error. @@ -1651,7 +1653,7 @@ def list_protocols(self) -> list[HostedProtocol]: A worker that does not host reflection (vgi-rpc raises ``ReflectionNotSupportedError``) is not an error: the result is a single - ``HostedProtocol("vgi.v2", "", "")``, since ``vgi.v2`` is the one + ``HostedProtocol("vgi.v3", "", "")``, since ``vgi.v3`` is the one protocol every worker hosts. The empty hash marks the entry as inferred. Returns: @@ -3203,6 +3205,70 @@ def table_function( except ClientError as e: raise self._client_error_with_stderr(e) from e.__cause__ + def catalog_query_execute( + self, + *, + attach_opaque_data: bytes, + prepared: CatalogQueryPrepareResult, + parameters: QueryParameters, + transaction_opaque_data: bytes | None = None, + ) -> Generator[pa.RecordBatch]: + """Execute a prepared query once through the ordinary single-reader TABLE stream. + + Every call starts a fresh execution. An error is propagated without a + retry or local fallback. Releasing the preparation is a separate action; + it may be reused with new values of exactly the same parameter schema. + Closing this generator early cancels its execution. + + Explicit statement preparations reuse this method. The caller must + consume the stream to EOS before reporting statement success or calling + commit. Constructing this lazy generator alone executes nothing. + Statement cancellation/failure requires whole-transaction rollback. + """ + prepared.__post_init__() + if prepared.status != "accepted" or prepared.prepared_query_token is None: + raise ValueError("query execution requires an accepted preparation") + if self._primary is None: + raise ClientError("Client not started. Call start() or use context manager.") + expected = read_query_schema(prepared.output_schema) + request = BindRequest( + function_name=QUERY_EXECUTOR, + schema_path=[], + function_type=FunctionType.TABLE, + arguments=Arguments( + positional=( + pa.scalar(prepared.prepared_query_token, type=pa.binary()), + pa.scalar(parameters.serialize_to_bytes(), type=pa.binary()), + ) + ), + attach_opaque_data=attach_opaque_data, + transaction_opaque_data=transaction_opaque_data, + ) + proxy = self._primary.proxy + bound = self._do_bind(proxy, request, None) + if not bound.output_schema.equals(expected, check_metadata=True): + raise ClientError("query bind changed its prepared output schema") + stream = self._do_init(proxy, request, bound, projection_ids=None, pushdown_filters_batch=None, phase=None) + self._primary.stream = stream + complete = False + try: + if stream.typed_header(GlobalInitResponse).max_workers != 1: + raise ClientError("catalog query requires exactly one result reader") + for output in stream: + if not output.batch.schema.equals(expected, check_metadata=True): + raise ClientError("query execution changed its prepared output schema") + yield output.batch + complete = True + except RpcError as error: + raise ClientError.from_rpc_error(error) from error + finally: + if not complete: + with contextlib.suppress(Exception): + stream.cancel() + stream.close() + if self._primary is not None and self._primary.stream is stream: + self._primary.stream = None + @property def supports_resumable_scan(self) -> bool: """Whether this client's transport can drive :meth:`table_scan_resumable`. @@ -3267,6 +3333,8 @@ def table_scan_resumable( f"table_scan_resumable requires the HTTP transport; this client uses {self._transport!r}. " "Use table_function() and keep the live stream in-process." ) + if function_name == QUERY_EXECUTOR and resume_token is not None: + raise ValueError("resume catalog queries with table_scan_continue; rebinding would start a new execution") if arguments is None: arguments = Arguments() if self._primary is None: diff --git a/vgi/codegen/_common.py b/vgi/codegen/_common.py index 4683cbad..44f386fb 100644 --- a/vgi/codegen/_common.py +++ b/vgi/codegen/_common.py @@ -22,6 +22,7 @@ import pyarrow as pa from vgi_rpc.rpc._types import MethodType, rpc_methods # type: ignore[attr-defined] +from vgi.catalog.capabilities import CatalogCapabilities from vgi.catalog.catalog_interface import ( AttachCatalogInfo, CatalogInfo, @@ -38,6 +39,30 @@ TableInfo, ViewInfo, ) +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintInfo, + ConstraintsApplyRequest, + ConstraintSnapshot, + ForeignKeyDefinition, +) +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot, ObjectDependency +from vgi.catalog.indexes import IndexDefinition, IndexKey +from vgi.catalog.layout import SortKey, SqlExpression, TableLayout, TableLayoutChange +from vgi.catalog.macros import MacroOverload, MacroParameter +from vgi.catalog.metadata_changes import CatalogMetadataChanges +from vgi.catalog.options import OptionChanges, OptionDefinitions, OptionState +from vgi.catalog.query import ( + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, + QueryEvaluationContext, + QueryObjectReference, +) +from vgi.catalog.sql import SqlStatement +from vgi.column_path import RequiredFilterPaths from vgi.invocation import GlobalInitResponse from vgi.protocol import ( AggregateBindRequest, @@ -70,6 +95,7 @@ TableFunctionCardinalityRequest, TableFunctionDynamicToStringRequest, TableFunctionPlanRequest, + TableFunctionProjectionRequest, TableFunctionStatisticsRequest, VgiProtocol, ) @@ -88,15 +114,52 @@ CopyFromFormatInfo, ) +# Shared successor-contract records are emitted in every SDK during protocol +# preparation, before the catalog methods using them are frozen. Keep a single +# inventory for schemas and typed models, including otherwise opaque IPC bodies. +SHARED_CONTRACT_TYPES: tuple[type, ...] = ( + MacroParameter, + MacroOverload, + ClientQueryCapabilities, + QueryCapabilities, + QueryEvaluationContext, + QueryObjectReference, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + CatalogCapabilities, + CatalogMetadataChanges, + CatalogObjectRef, + ObjectDependency, + DependencySnapshot, + OptionDefinitions, + SqlExpression, + SqlStatement, + ForeignKeyDefinition, + ConstraintDefinition, + ConstraintInfo, + ConstraintChange, + ConstraintSnapshot, + ConstraintsApplyRequest, + IndexKey, + IndexDefinition, + SortKey, + TableLayout, + TableLayoutChange, + OptionChanges, + OptionState, + RequiredFilterPaths, +) + # Extra dataclasses whose ARROW_SCHEMA is referenced by methods whose return # type is `bytes` (raw IPC). Not inferable from method signatures alone. EXTRA_RESPONSE_TYPES: tuple[type, ...] = ( + *SHARED_CONTRACT_TYPES, ScanFunctionResult, # catalog_table_{scan,insert,update,delete}_function_get ScanBranchesResult, # catalog_table_scan_branches_get (top-level wrapper) ScanBranch, # one entry inside ScanBranchesResult.branches (binary blob) ScanSplit, # one entry inside PlanResponse.splits (binary blob) AttachCatalogInfo, # one entry inside CatalogAttachResult.attach_catalogs (binary blob) - ForeignKeyInfo, # one entry inside TableInfo.foreign_key_constraints (binary blob) + ForeignKeyInfo, # legacy convenience record; no longer carried by TableInfo # The struct row type of CatalogContentsResponse.schemas. It is inline (a # struct column, not a blob), so the catalog_contents result schema already # spells it out; it is also emitted standalone so each language's generated @@ -117,6 +180,8 @@ # Emitted for the languages that need to CHECK these shapes: Java (its records # mirror them) and C++ (it hand-builds them, and had no way to verify that). REQUEST_TYPES: tuple[type, ...] = ( + ConstraintsApplyRequest, + CatalogQueryPrepareRequest, AggregateBindRequest, AggregateCombineRequest, AggregateDestructorRequest, @@ -146,6 +211,7 @@ TableFunctionCardinalityRequest, TableFunctionDynamicToStringRequest, TableFunctionPlanRequest, + TableFunctionProjectionRequest, TableFunctionStatisticsRequest, ) diff --git a/vgi/codegen/_registry.py b/vgi/codegen/_registry.py index 2a4d54ec..066c0a5b 100644 --- a/vgi/codegen/_registry.py +++ b/vgi/codegen/_registry.py @@ -1,11 +1,11 @@ # Copyright 2025, 2026 Query Farm LLC - https://query.farm -"""The language-neutral model behind every SDK's generated ``vgi.v2`` registry. +"""The language-neutral model behind every SDK's generated ``vgi.v3`` registry. Why the registry is generated ----------------------------- -``vgi.v2`` is the unit of optionality: every SDK hosts *every* method, with +``vgi.v3`` is the unit of optionality: every SDK hosts *every* method, with exactly the reference's method type and params / result / header schemas, so ``vgi_rpc.Reflection.v1`` reports one protocol hash everywhere. A method an SDK does not implement is still registered and answers ``UNIMPLEMENTED`` / @@ -61,7 +61,7 @@ here. Every backend's output is three things: **a complete declaration** -(interface, trait, abstract class) of every ``vgi.v2`` method with its exact +(interface, trait, abstract class) of every ``vgi.v3`` method with its exact types; **a default implementation** in which every method answers the SDK's ``UNIMPLEMENTED``, which SDK code inherits and overrides; and **the registration** that hands the declaration to the vgi-rpc server. In Java and @@ -150,7 +150,7 @@ def record_name(annotation: object) -> str | None: @dataclass(frozen=True) class RegistryParam: - """One wire parameter of a ``vgi.v2`` method, in params-schema order. + """One wire parameter of a ``vgi.v3`` method, in params-schema order. Attributes: name: The wire name (snake_case), which is also the params-schema field name. @@ -188,7 +188,7 @@ class SchemaNames: @dataclass(frozen=True) class RegistryMethod: - """One ``vgi.v2`` method, everything a registry needs to declare it. + """One ``vgi.v3`` method, everything a registry needs to declare it. Attributes: name: The wire method name. @@ -466,10 +466,10 @@ def records_used(methods: Iterable[RegistryMethod], *, headers: bool = True) -> def check_record_table( table: Mapping[str, str], methods: Sequence[RegistryMethod], *, what: str, headers: bool ) -> None: - """Refuse a backend name-table entry for a record no ``vgi.v2`` method carries (a typo binds nothing).""" + """Refuse a backend name-table entry for a record no ``vgi.v3`` method carries (a typo binds nothing).""" stale = sorted(set(table) - records_used(methods, headers=headers)) if stale: - raise GeneratorError(f"{what} names records no vgi.v2 method carries: {stale}") + raise GeneratorError(f"{what} names records no vgi.v3 method carries: {stale}") def check_raw_results( diff --git a/vgi/codegen/_registry_backend.py b/vgi/codegen/_registry_backend.py index a9cd8504..ea401153 100644 --- a/vgi/codegen/_registry_backend.py +++ b/vgi/codegen/_registry_backend.py @@ -214,7 +214,7 @@ def rebuild(derived: Sequence[DerivedMethod]) -> list[RegistryMethod]: out: list[RegistryMethod] = [] for d in derived: base = model.get(d.name) - expect(base is not None, f"the rendered file registers {d.name}, which is not a vgi.v2 method") + expect(base is not None, f"the rendered file registers {d.name}, which is not a vgi.v3 method") assert base is not None expect(d.name not in {m.name for m in out}, f"{d.name} is registered twice") names = [f.name for f in d.params] diff --git a/vgi/codegen/catalog_contract_fixtures.py b/vgi/codegen/catalog_contract_fixtures.py new file mode 100644 index 00000000..56411667 --- /dev/null +++ b/vgi/codegen/catalog_contract_fixtures.py @@ -0,0 +1,845 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Emit shared Arrow IPC codec vectors for the catalog contract records. + +Every worker SDK decodes these Python-written records through its real codec. +Binary value payloads must survive byte-for-byte, including nested metadata. +These vectors prove representation; option mutation and path-policy semantics +have separate reference tests and are not implied by a codec round trip. +""" + +from __future__ import annotations + +import base64 +import json +from dataclasses import replace +from typing import Any, TextIO, cast + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass +from vgi_rpc.rpc._types import rpc_methods + +from vgi.catalog.capabilities import ( + ROW_SCALAR_EXPRESSION, + SCHEMA_OPTIONS_CREATE, + SCHEMA_OPTIONS_READ, + CatalogCapabilities, +) +from vgi.catalog.catalog_interface import ( + AttachOpaqueData, + CatalogAttachResult, + IndexInfo, + MacroInfo, + MacroType, + OnConflict, + TableInfo, + ViewInfo, +) +from vgi.catalog.constraints import ( + ConstraintChange, + ConstraintDefinition, + ConstraintEnforcement, + ConstraintInfo, + ConstraintKind, + ConstraintOperation, + ConstraintsApplyRequest, + ConstraintSnapshot, + ConstraintValidation, + ForeignKeyDefinition, +) +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot, ObjectDependency +from vgi.catalog.indexes import IndexConstraintType, IndexDefinition, IndexKey +from vgi.catalog.layout import ( + LayoutChangeMode, + NullOrder, + SortDirection, + SortKey, + SqlExpression, + TableLayout, + TableLayoutChange, +) +from vgi.catalog.macros import MACRO_DEFAULT_PROFILE, MACRO_PROFILE, MacroOverload, MacroParameter +from vgi.catalog.metadata_changes import CatalogMetadataChanges +from vgi.catalog.options import OptionChanges, OptionDefinitions, OptionState +from vgi.catalog.query import ( + QUERY_AUTOMATIC, + QUERY_ENCODING, + QUERY_EXPLICIT, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, + QueryEvaluationContext, + QueryObjectReference, +) +from vgi.catalog.sql import ColumnExpressions, SqlStatement, with_column_expressions +from vgi.column_path import RequiredFilterPaths +from vgi.logical_types import tuple_field, variant_field +from vgi.protocol import IndexCreateRequest, MacroCreateRequest, TableCreateRequest, VgiProtocol +from vgi.query_parameters import QueryParameters +from vgi.typed_values import TypedValueBatch + + +def attach_records() -> dict[str, CatalogAttachResult]: + """Preserve absent, ordinary and literal dotted default schema identifiers.""" + return { + f"attach_default_schema_{name}": CatalogAttachResult( + attach_opaque_data=AttachOpaqueData(b"attachment"), + supports_transactions=False, + supports_time_travel=False, + catalog_version_frozen=True, + catalog_version=7, + default_schema=default, + resolved_data_version=None, + resolved_implementation_version=None, + ) + for name, default in (("none", None), ("main", "main"), ("dotted", "sales.archive")) + } + + +def column_metadata_schemas() -> dict[str, dict[str, str]]: + """Share accepted and rejected metadata through real Arrow schema messages.""" + empty = {b"comment": b"", b"tags": b"{}", b"provider.note": b"keep me"} + literal = { + b"comment": "A café column\x00 with a literal dot.".encode(), + b"tags": json.dumps({"a.b": 'literal "value"', "": "", "雪": "\x00"}, ensure_ascii=False).encode(), + b"provider.note": b"keep me too", + } + nested = pa.struct( + [ + pa.field("absent", pa.int32()), + pa.field("empty", pa.string(), metadata=empty), + pa.field("literal.dot", pa.int64(), metadata=literal), + pa.field("list", pa.list_(pa.field("item", pa.string(), metadata=empty))), + pa.field("map", pa.map_(pa.string(), cast(Any, pa.field("value", pa.int64(), metadata=literal)))), + pa.field( + "dictionary", + pa.dictionary(pa.int32(), cast(Any, pa.struct([pa.field("child", pa.int32(), metadata=empty)]))), + ), + ] + ) + valid = { + "absent": pa.schema([pa.field("a.b", pa.int32())]), + "empty": pa.schema([pa.field("a.b", pa.int32(), metadata=empty)]), + "literal": pa.schema([pa.field("a.b", pa.int32(), metadata=literal)], metadata={b"provider": b"retained"}), + "nested": pa.schema([pa.field("record", nested, metadata=empty)], metadata={b"provider": b"retained"}), + } + invalid_tags = { + "duplicate_keys": b'{"key":"one","key":"two"}', + "escaped_duplicate_keys": b'{"key":"one","\\u006bey":"two"}', + "array": b"[]", + "null": b"null", + "number": b"1", + "string": b'"text"', + "number_value": b'{"key":1}', + "null_value": b'{"key":null}', + "boolean_value": b'{"key":true}', + "object_value": b'{"key":{}}', + "array_value": b'{"key":[]}', + "malformed": b'{"key":', + "trailing": b"{} trailing", + "invalid_utf8": b'{"key":"\xff"}', + "surrogate_key": b'{"\\ud800":"value"}', + "surrogate_value": b'{"key":"\\udfff"}', + } + invalid = { + f"tags_{name}": pa.schema([pa.field("a.b", pa.int32(), metadata={b"tags": value})]) + for name, value in invalid_tags.items() + } + invalid["comment_invalid_utf8"] = pa.schema([pa.field("a.b", pa.int32(), metadata={b"comment": b"\xff"})]) + for name, value in {"overlong": b"\xc0\xaf", "surrogate": b"\xed\xa0\x80", "truncated": b"\xe2\x82"}.items(): + invalid[f"comment_{name}_utf8"] = pa.schema([pa.field("a.b", pa.int32(), metadata={b"comment": value})]) + for name, key, value in (("comment", b"comment", b""), ("tags", b"tags", b"{}"), ("opaque", b"provider", b"keep")): + pairs: Any = [(key, value), (key, value)] + invalid[f"duplicate_arrow_{name}"] = pa.schema( + [pa.field("a.b", pa.int32(), metadata=cast(Any, pa.KeyValueMetadata(pairs)))] + ) + bad_child = pa.field("child", pa.string(), metadata={b"tags": invalid_tags["duplicate_keys"]}) + for name, kind in { + "struct": pa.struct([bad_child]), + "list": pa.list_(bad_child), + "map": pa.map_(pa.string(), cast(Any, bad_child)), + "dictionary": pa.dictionary(pa.int32(), cast(Any, pa.struct([bad_child]))), + }.items(): + invalid[f"nested_{name}"] = pa.schema([pa.field("outer", kind)]) + return { + group: { + name: base64.b64encode(schema.serialize().to_pybytes()).decode("ascii") for name, schema in schemas.items() + } + for group, schemas in (("valid", valid), ("invalid", invalid)) + } + + +def generated_column_schemas() -> dict[str, Any]: + """Share canonical virtual definitions and rejection cases without evaluating SQL.""" + owner = ["main"] + expression = SqlExpression( + sql="n * 2", profile=ROW_SCALAR_EXPRESSION, schema_path=owner, evaluation_context=QueryEvaluationContext() + ) + payload = expression.serialize_to_bytes() + encoded = base64.b64encode(payload) + expression_key = b"vgi.column.generated.v1" + category_key = b"vgi.column.generation.v1" + metadata = {expression_key: encoded, category_key: b"virtual", b"comment": b"", b"provider.note": b"retained"} + + def field(name: str, sql: str) -> pa.Field[Any]: + return with_column_expressions( + pa.field(name, pa.int64()), ColumnExpressions(generated=replace(expression, sql=sql), generation="virtual") + ) + + valid = { + "ordinary": pa.schema([pa.field("n", pa.int64())]), + "interleaved_chain": pa.schema( + [ + pa.field("n", pa.int64()), + pa.field("doubled", pa.int64(), metadata=metadata), + pa.field("tail", pa.int64()), + field("last", "doubled + tail"), + ], + metadata={b"provider": b"retained"}, + ), + "literal_dotted_column": pa.schema([pa.field("a.b", pa.int64()), field("computed", '"a.b" * 2')]), + } + + def malformed_record(**changes: Any) -> bytes: + # Deliberately bypass dataclass validation to exercise real wire readers. + row = pa.ipc.open_stream(payload).read_next_batch().to_pylist()[0] + row.update(changes) + batch = pa.RecordBatch.from_pylist([row], schema=SqlExpression.ARROW_SCHEMA) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, batch.schema) as writer: + writer.write_batch(batch) + return base64.b64encode(sink.getvalue().to_pybytes()) + + invalid_metadata = { + "missing_generation": {expression_key: encoded}, + "missing_expression": {category_key: b"virtual"}, + "unknown_generation": {**metadata, category_key: b"unknown"}, + "stored_unsupported": {**metadata, category_key: b"stored"}, + "default_conflict": {**metadata, b"vgi.column.default.v1": encoded}, + "legacy_conflict": {**metadata, b"generated_expression": b"n * 2"}, + "legacy_generated_only": {b"generated_expression": b"n * 2"}, + "unknown_metadata_version": {**metadata, b"vgi.column.generated.v2": encoded}, + "invalid_base64": {**metadata, expression_key: b"not_base64!"}, + "base64_whitespace": {**metadata, expression_key: encoded + b"\n"}, + "empty_payload": {**metadata, expression_key: b""}, + "invalid_ipc": {**metadata, expression_key: base64.b64encode(b"not an IPC record")}, + "invalid_utf8_sql": {**metadata, expression_key: base64.b64encode(payload.replace(b"n * 2", b"n \xff 2"))}, + "trailing_ipc": {**metadata, expression_key: base64.b64encode(payload + b"trailing")}, + "wrong_record": {**metadata, expression_key: base64.b64encode(QueryEvaluationContext().serialize_to_bytes())}, + "missing_context": {**metadata, expression_key: malformed_record(evaluation_context=None)}, + "invalid_context": { + **metadata, + expression_key: malformed_record( + evaluation_context={ + "timezone": "UTC", + "collation": "binary", + "default_order": "sideways", + "default_null_order": "nulls_last", + "preserve_insertion_order": True, + } + ), + }, + "empty_sql": {**metadata, expression_key: malformed_record(sql="")}, + "owner_mismatch": { + **metadata, + expression_key: base64.b64encode(replace(expression, schema_path=["elsewhere"]).serialize_to_bytes()), + }, + "unsupported_profile": { + **metadata, + expression_key: base64.b64encode(replace(expression, profile="other.expression.v1").serialize_to_bytes()), + }, + } + invalid = { + name: pa.schema([pa.field("n", pa.int64()), pa.field("computed", pa.int64(), metadata=annotations)]) + for name, annotations in invalid_metadata.items() + } + return { + "owner_schema_path": owner, + **{ + group: { + name: base64.b64encode(schema.serialize().to_pybytes()).decode("ascii") + for name, schema in schemas.items() + } + for group, schemas in (("valid", valid), ("invalid", invalid)) + }, + } + + +def column_requests() -> dict[str, bytes]: + """Encode literal and nested targets through each actual RPC parameter schema.""" + methods = rpc_methods(VgiProtocol) + result = {} + scope = ["one.two", "nested"] + default = SqlExpression( + sql="7", profile=ROW_SCALAR_EXPRESSION, schema_path=scope, evaluation_context=QueryEvaluationContext() + ) + for operation in ( + "column_add", + "column_drop", + "column_rename", + "column_type_change", + "column_default_set", + "column_default_drop", + "column_comment_set", + "not_null_set", + "not_null_drop", + ): + schema = methods[f"catalog_table_{operation}"].params_schema + for variant, path in (("literal", ["a.b"]), ("nested", ["a", 'b."quoted'])): + nested = variant == "nested" + field = pa.field(path[-1], pa.int32()) + if operation == "column_add": + field = with_column_expressions(field, ColumnExpressions(default=default)) + definition = pa.schema([field]) + values = dict( + attach_opaque_data=b"attach", + transaction_opaque_data=b"transaction" if nested else None, + schema_path=scope, + name="table.with.dot", + column_path=path, + column_definition=definition.serialize().to_pybytes(), + new_column_name='new."name', + expression=( + None + if operation == "column_type_change" and not nested + else SqlExpression( + sql="7", + profile=ROW_SCALAR_EXPRESSION, + schema_path=scope, + evaluation_context=QueryEvaluationContext(), + ).serialize_to_bytes() + ), + comment="kept" if nested else None, + ignore_not_found=nested, + if_column_exists=nested, + if_column_not_exists=nested, + cascade=nested, + ) + batch = pa.RecordBatch.from_pylist([{field.name: values[field.name] for field in schema}], schema=schema) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, schema) as writer: + writer.write_batch(batch) + result[f"{operation}_{variant}"] = sink.getvalue().to_pybytes() + return result + + +def logical_and_macro_records() -> dict[str, TypedValueBatch | ArrowSerializableDataclass]: + """Cover dynamic type identity and complete macro families in every SDK corpus.""" + + def scalar(field: pa.Field[Any], value: Any) -> TypedValueBatch: + return TypedValueBatch( + pa.RecordBatch.from_arrays([pa.array([value], type=field.type)], schema=pa.schema([field])) + ) + + pair = tuple_field("pair", [pa.field("", pa.uint64()), pa.field("", pa.string())]) + tuple_values = scalar(pair, {"element1": 2**64 - 1, "element2": None}) + struct_values = scalar(pair.remove_metadata(), {"element1": 2**64 - 1, "element2": None}) + # Canonical Parquet Variant v1 bytes, carried opaquely by Arrow's + # arrow.parquet.variant extension. These are not nested Arrow IPC streams. + variant_null = {"metadata": bytes.fromhex("010000"), "value": bytes.fromhex("00")} + absent = {"metadata": bytes.fromhex("010000"), "value": bytes.fromhex("020000")} + null_member = { + "metadata": bytes.fromhex("0101000770726573656e74"), + "value": bytes.fromhex("020100000100"), + } + scope = ["one.two", "nested"] + literal = scalar(pa.field("fallback", pa.int64(), metadata={b"domain": b"retained"}), None) + parameter = MacroParameter( + name="fallback", declared_type=literal.schema, default_value=literal, documentation="A typed NULL default" + ) + expression_parameter = MacroParameter( + name="scale", + declared_type=pa.schema([pa.field("scale", pa.int32())]), + default_expression=SqlExpression( + evaluation_context=QueryEvaluationContext(), sql="2 + 3", profile=MACRO_DEFAULT_PROFILE, schema_path=scope + ), + ) + overload = MacroOverload( + evaluation_context=QueryEvaluationContext(), + parameters=[MacroParameter(name="x"), parameter, expression_parameter], + definition="coalesce(x, fallback) * scale", + profile=MACRO_PROFILE, + schema_path=scope, + macro_type="scalar", + ) + second = MacroOverload( + evaluation_context=QueryEvaluationContext(), + parameters=[MacroParameter(name="x", declared_type=pa.schema([pa.field("x", pa.string())]))], + definition="upper(x)", + profile=MACRO_PROFILE, + schema_path=scope, + macro_type="scalar", + ) + published = replace( + overload, + parameters=[ + overload.parameters[0], + parameter, + replace( + expression_parameter, default_expression=None, default_value=scalar(pa.field("scale", pa.int32()), 5) + ), + ], + ) + common: dict[str, Any] = dict(name="macro.with.dot", schema_path=scope, macro_type=MacroType.SCALAR) + single = MacroOverload( + evaluation_context=QueryEvaluationContext(), + parameters=[MacroParameter(name="x")], + definition="x + 1", + profile=MACRO_PROFILE, + schema_path=scope, + macro_type="scalar", + ) + return { + "logical_tuple": tuple_values, + "logical_struct": struct_values, + "logical_variant_sql_null": scalar(variant_field("value"), None), + "logical_variant_null": scalar(variant_field("value"), variant_null), + "logical_variant_absent": scalar(variant_field("value"), absent), + "logical_variant_null_member": scalar(variant_field("value"), null_member), + "macro_parameter": parameter, + "macro_parameter_expression": expression_parameter, + "macro_parameter_untyped": MacroParameter(name="x"), + "macro_overload": overload, + "macro_create": MacroCreateRequest( + **common, + attach_opaque_data=b"attach", + on_conflict=OnConflict.REPLACE, + transaction_opaque_data=b"transaction", + overloads=[overload, second], + ), + "macro_create_single": MacroCreateRequest( + **common, attach_opaque_data=b"attach", overloads=[single], on_conflict=OnConflict.ERROR + ), + "macro_info": MacroInfo(**common, overloads=[published, second], comment=None, tags={}), + "macro_info_single": MacroInfo(**common, overloads=[single], comment=None, tags={}), + } + + +def constraint_records() -> dict[str, ArrowSerializableDataclass]: + """Exercise every canonical variant, opaque identity and binary revision.""" + table = CatalogObjectRef(kind="table", schema_path=["one.two", "nested"], name='child."table') + parent = replace(table, name="parent.with.dot") + ref = CatalogObjectRef(kind="constraint", schema_path=table.schema_path, owner_table=table.name, name="key.01") + referenced = replace(ref, owner_table=parent.name, name="parent.key.01") + context = QueryEvaluationContext( + default_order="desc", default_null_order="nulls_first", preserve_insertion_order=False + ) + expression = SqlExpression( + sql='"a.b" > 0', + profile="vgi.expression.duckdb.scalar.v1", + schema_path=table.schema_path, + evaluation_context=context, + ) + foreign = ForeignKeyDefinition(referenced_table=parent, referenced_column_paths=[["parent.id"], ["record", "id"]]) + definitions = { + "not_null": ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[["a.b"]]), + "unique": ConstraintDefinition(kind=ConstraintKind.UNIQUE, column_paths=[["a.b"], ["record", "id"]]), + "primary": ConstraintDefinition( + kind=ConstraintKind.PRIMARY_KEY, column_paths=[["id"]], declared_name="primary.label" + ), + "check": ConstraintDefinition( + kind=ConstraintKind.CHECK, check_expression=expression, declared_name="check.label" + ), + "foreign": ConstraintDefinition( + kind=ConstraintKind.FOREIGN_KEY, column_paths=[["a.b"], ["record", "id"]], foreign_key=foreign + ), + } + info = ConstraintInfo( + ref=ref, + definition=definitions["foreign"], + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + referenced_constraint=referenced, + ) + changes = { + "add": ConstraintChange( + operation=ConstraintOperation.ADD, + definition=definitions["check"], + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + ), + "drop": ConstraintChange(operation=ConstraintOperation.DROP, ref=ref, cascade=False), + "rename": ConstraintChange(operation=ConstraintOperation.RENAME, ref=ref, declared_name='label."new'), + "unname": ConstraintChange(operation=ConstraintOperation.RENAME, ref=ref, declared_name=None), + "replace": ConstraintChange( + operation=ConstraintOperation.REPLACE, + ref=ref, + definition=definitions["unique"], + enforcement=ConstraintEnforcement.METADATA_ONLY, + validation=ConstraintValidation.UNVALIDATED, + ), + } + revision = b"\x00revision\xff\x80" + return { + "sql_statement": SqlStatement( + sql='SELECT "a.b" FROM "child.""table"', + profile="vgi.statement.duckdb.query.v1", + schema_path=table.schema_path, + evaluation_context=context, + ), + "constraint_ref": ref, + "foreign_key_definition": foreign, + **{f"constraint_definition_{name}": value for name, value in definitions.items()}, + "constraint_info": info, + "constraint_info_untrusted": ConstraintInfo( + ref=replace(ref, name="key.02"), + definition=definitions["unique"], + enforcement=ConstraintEnforcement.METADATA_ONLY, + validation=ConstraintValidation.FUTURE_WRITES, + ), + **{f"constraint_change_{name}": value for name, value in changes.items()}, + "constraint_snapshot": ConstraintSnapshot( + table=table, definition_revision=revision, constraints=[info.serialize_to_bytes()] + ), + "constraint_snapshot_empty": ConstraintSnapshot(table=table, definition_revision=revision, constraints=[]), + "constraints_apply": ConstraintsApplyRequest( + attach_opaque_data=b"attachment", + transaction_opaque_data=b"transaction", + table=table, + expected_revision=revision, + changes=[changes["add"].serialize_to_bytes(), changes["rename"].serialize_to_bytes()], + ), + "constraints_apply_autocommit": ConstraintsApplyRequest( + attach_opaque_data=b"attachment", + table=table, + expected_revision=revision, + changes=[changes["unname"].serialize_to_bytes()], + ), + } + + +def index_records(values: TypedValueBatch) -> dict[str, ArrowSerializableDataclass]: + """Typed index keys and options preserve complete raw object identities.""" + scope = ["one.two", "nested"] + table = CatalogObjectRef(kind="table", schema_path=scope, name='table."name') + column = IndexKey(column_path=["a.b"]) + nested = IndexKey(column_path=["record", "a.b"]) + expression = IndexKey( + expression=SqlExpression( + sql='"a.b" + 1', + profile="vgi.expression.duckdb.scalar.v1", + schema_path=scope, + evaluation_context=QueryEvaluationContext(), + ) + ) + definition = IndexDefinition( + table=table, + algorithm="fixture.typed-index.v1", + keys=[column.serialize_to_bytes(), expression.serialize_to_bytes()], + options=values, + constraint_type=IndexConstraintType.NONE, + ) + state = OptionState(values=values, explicit_names=["optional"]) + primary = replace( + definition, + keys=[column.serialize_to_bytes()], + constraint_type=IndexConstraintType.PRIMARY, + associated_constraint=CatalogObjectRef( + kind="constraint", schema_path=scope, name="fixture.primary", owner_table=table.name + ), + ) + return { + "index_key_column": column, + "index_key_nested": nested, + "index_key_expression": expression, + "index_definition": definition, + "index_definition_primary": primary, + "index_info": IndexInfo( + name='index."name', + schema_path=scope, + definition=definition, + options=state, + comment="", + tags={"owner": "index.fixture"}, + ), + "index_create": IndexCreateRequest( + attach_opaque_data=b"attach", + schema_path=scope, + name='index."name', + definition=definition, + on_conflict=OnConflict.REPLACE, + transaction_opaque_data=b"txn\0\xff", + ), + "index_create_cross_schema": IndexCreateRequest( + attach_opaque_data=b"attach", + schema_path=["indexes"], + name="elsewhere", + definition=definition, + ), + } + + +def render() -> str: + """Return deterministic JSON with populated and empty record encodings.""" + schema = pa.schema( + [ + pa.field("optional", pa.int32(), metadata={b"logical": b"example.optional"}), + pa.field( + "record", + pa.struct([pa.field("a.b", pa.string(), nullable=False, metadata={b"nested": b"retained"})]), + metadata={b"logical": b"example.record"}, + ), + ], + metadata={b"provider": b"catalog-contracts/v1"}, + ) + values = TypedValueBatch( + pa.RecordBatch.from_pylist([{"optional": None, "record": {"a.b": 'literal."name'}}], schema=schema) + ) + empty = TypedValueBatch.empty(metadata={b"provider": b"no-options"}) + expression = SqlExpression( + evaluation_context=QueryEvaluationContext(), + sql='"a.b" + 1', + profile="vgi.expression.duckdb.scalar.v1", + schema_path=["one.two", "nested"], + ) + default_expression = replace(expression, sql="42") + expression_metadata = TypedValueBatch( + pa.RecordBatch.from_arrays( + [pa.array([None], type=pa.int64()), pa.array([None], type=pa.int64())], + schema=pa.schema( + [ + with_column_expressions(pa.field("a.b", pa.int64()), ColumnExpressions(default=default_expression)), + with_column_expressions( + pa.field("computed", pa.int64()), ColumnExpressions(generated=expression, generation="virtual") + ), + ] + ), + ) + ) + sort_key = SortKey(expression=expression, direction=SortDirection.DESC, null_order=NullOrder.FIRST) + layout = TableLayout.from_expressions(partition_by=[expression], sorted_by=[sort_key]) + table_args: dict[str, Any] = dict( + name="table.with.dot", schema_path=expression.schema_path, columns=schema.serialize().to_pybytes() + ) + table_definition = ConstraintDefinition(kind=ConstraintKind.NOT_NULL, column_paths=[["record", "a.b"]]) + table_constraint = ConstraintInfo( + ref=CatalogObjectRef( + kind="constraint", + schema_path=expression.schema_path, + owner_table=table_args["name"], + name="fixture-stable-key", + ), + definition=table_definition, + enforcement=ConstraintEnforcement.ENFORCED, + validation=ConstraintValidation.ALL_ROWS, + ) + records: dict[str, TypedValueBatch | ArrowSerializableDataclass] = { + "view_info": ViewInfo( + name="view.with.dot", + schema_path=["one.two", "nested"], + definition="SELECT 1 AS original, 2 AS second", + aliases=["a.b", 'with"quote'], + column_comments={"a.b": "kept"}, + comment="view comment", + tags={"team": "one"}, + ), + "view_info_barrier": ViewInfo( + name="barrier", + schema_path=["main"], + definition="SELECT 1 AS original", + comment=None, + tags={}, + security="barrier", + ), + "view_info_implicit": ViewInfo( + name="implicit", schema_path=["main"], definition="SELECT 1 AS original", comment=None, tags={} + ), + "capabilities": CatalogCapabilities(read=[SCHEMA_OPTIONS_READ], ddl=[SCHEMA_OPTIONS_CREATE]), + "empty_capabilities": CatalogCapabilities(), + "definitions": OptionDefinitions( + schema=schema, defaults=values, create_names=schema.names, alter_names=["optional"] + ), + "values": values, + "empty_values": empty, + "metadata_patch": CatalogMetadataChanges( + set_comment=True, + comment="", + set_tags={"a.b": "", "__proto__": "literal", "": "empty-key"}, + remove_tags=["old", "OLD"], + ), + "metadata_clear_comment": CatalogMetadataChanges(set_comment=True), + "metadata_noop": CatalogMetadataChanges(), + "changes": OptionChanges(set_values=values, reset_names=["reset.me"]), + "reset_only": OptionChanges(reset_names=["reset.me"]), + "state": OptionState(values=values, explicit_names=["optional"]), + "empty_state": OptionState(values=empty), + "paths": RequiredFilterPaths(groups=[[["a.b"], ["a", "b"]], [["record", 'literal."name']]]), + "empty_paths": RequiredFilterPaths(), + "expression": expression, + "column_default_expression": default_expression, + "column_generated_expression": expression, + "column_expression_metadata": expression_metadata, + "sort_key": sort_key, + "layout": layout, + "empty_layout": TableLayout(), + "layout_change": TableLayoutChange(mode=LayoutChangeMode.REWRITE, partition_by=[], sorted_by=layout.sorted_by), + "layout_clear": TableLayoutChange(mode=LayoutChangeMode.FUTURE_WRITES, sorted_by=[]), + "table_create": TableCreateRequest( + constraint_definitions=[table_definition.serialize_to_bytes()], + **table_args, + attach_opaque_data=b"attach", + on_conflict=OnConflict.ERROR, + comment="kept", + tags={"team": "one"}, + options=values, + layout=layout, + ), + "table_create_legacy": TableCreateRequest( + constraint_definitions=[], **table_args, attach_opaque_data=b"attach", on_conflict=OnConflict.ERROR + ), + "table_info": TableInfo( + **table_args, + comment="kept", + tags={"team": "one"}, + constraints=[table_constraint.serialize_to_bytes()], + definition_revision=b"revision\0\xff", + required_filters=[[["optional"], ["record", "a.b"]]], + options=OptionState(values=values, explicit_names=["optional"]), + layout=layout, + ), + } + root = CatalogObjectRef(kind="schema", schema_path=[], name="one.two") + nested = CatalogObjectRef(kind="schema", schema_path=["one.two"], name="nested") + table = CatalogObjectRef(kind="table", schema_path=["one.two", "nested"], name='table."name') + trigger = CatalogObjectRef(kind="trigger", schema_path=table.schema_path, name="on.change", owner_table=table.name) + owned = ObjectDependency( + dependent=trigger.serialize_to_bytes(), subject=table.serialize_to_bytes(), blocks_alter=True, kind="owned" + ) + edges = [ + ObjectDependency(dependent=nested.serialize_to_bytes(), subject=root.serialize_to_bytes(), blocks_alter=False), + ObjectDependency(dependent=table.serialize_to_bytes(), subject=nested.serialize_to_bytes(), blocks_alter=False), + owned, + ] + records.update( + object_ref=table, + trigger_ref=trigger, + object_dependency=owned, + dependency_snapshot=DependencySnapshot( + catalog_version=42, + objects=[r.serialize_to_bytes() for r in [root, nested, table, trigger]], + dependencies=[e.serialize_to_bytes() for e in edges], + ), + empty_dependency_snapshot=DependencySnapshot(catalog_version=0), + ) + index = CatalogObjectRef(kind="index", schema_path=table.schema_path, name="key.index") + literal = ObjectDependency( + dependent=index.serialize_to_bytes(), + subject=table.serialize_to_bytes(), + blocks_alter=True, + subject_column_paths=[["a.b"]], + ) + nested_column = ObjectDependency( + dependent=index.serialize_to_bytes(), + subject=table.serialize_to_bytes(), + blocks_alter=True, + subject_column_paths=[["record", "a.b"]], + ) + generated = ObjectDependency( + dependent=table.serialize_to_bytes(), + subject=table.serialize_to_bytes(), + blocks_alter=True, + subject_column_paths=[["a.b"], ["record", "a.b"]], + dependent_column_path=["generated"], + ) + generated_chain = ObjectDependency( + dependent=table.serialize_to_bytes(), + subject=table.serialize_to_bytes(), + blocks_alter=True, + subject_column_paths=[["generated"]], + dependent_column_path=["second.generated"], + ) + records.update( + index_ref=index, + dependency_membership=edges[0], + dependency_column_literal=literal, + dependency_column_nested=nested_column, + dependency_generated=generated, + dependency_generated_chain=generated_chain, + dependency_column_snapshot=DependencySnapshot( + catalog_version=43, + objects=[table.serialize_to_bytes(), index.serialize_to_bytes()], + dependencies=[e.serialize_to_bytes() for e in [literal, generated, generated_chain]], + ), + ) + records.update(attach_records()) + records.update(logical_and_macro_records()) + records.update(constraint_records()) + records.update(index_records(values)) + encoded = {name: record.serialize_to_bytes() for name, record in records.items()} + parameters = QueryParameters.from_batch(values.batch, mode="named") + context = QueryEvaluationContext() + query_offer = ClientQueryCapabilities( + encodings=[QUERY_ENCODING], + semantic_profiles=[QUERY_AUTOMATIC, QUERY_EXPLICIT], + engine_version="fixture-client-build", + ) + query_capabilities = QueryCapabilities( + encoding=QUERY_ENCODING, + semantic_profiles=query_offer.semantic_profiles, + execution_modes=["automatic", "explicit"], + backend_engine="duckdb", + backend_version="fixture-provider-build", + capability_revision="policy-1", + default_evaluation_context=context, + ) + reference = QueryObjectReference(object=table, expected_schema=schema.serialize().to_pybytes()) + prepare = CatalogQueryPrepareRequest( + attach_opaque_data=b"attachment", + transaction_opaque_data=b"transaction", + encoding=QUERY_ENCODING, + semantic_profile=QUERY_AUTOMATIC, + mode="automatic", + capability_revision="policy-1", + query='SELECT * FROM "table"', + parameter_schema=parameters.schema_bytes(), + evaluation_context=context, + catalog_version=0, + catalog_etag="", + referenced_objects=[reference], + expected_output_schema=schema.serialize().to_pybytes(), + required_ordering="query_order", + ) + accepted = CatalogQueryPrepareResult( + status="accepted", + prepared_query_token=b"opaque-preparation", + output_schema=prepare.expected_output_schema, + result_ordering="query_order", + validated_catalog_version=0, + validated_catalog_etag="", + context_digest=context.digest(prepare.encoding, prepare.semantic_profile), + expires_at_epoch_ms=2_000_000_000_000, + estimated_rows=0, + estimated_bytes=0, + ) + query_records: dict[str, ArrowSerializableDataclass | QueryParameters] = dict( + query_offer=query_offer, + query_capabilities=query_capabilities, + query_context=context, + query_object_reference=reference, + query_prepare=prepare, + query_accepted=accepted, + query_declined=CatalogQueryPrepareResult(status="declined", reason_code="unsupported_query"), + query_parameters=parameters, + query_empty_parameters=QueryParameters.empty(), + ) + encoded.update({name: record.serialize_to_bytes() for name, record in query_records.items()}) + encoded.update(column_requests()) + return ( + json.dumps( + { + "format_version": 1, + "generator": "vgi.codegen.catalog_contract_fixtures", + "records": {name: base64.b64encode(payload).decode("ascii") for name, payload in encoded.items()}, + "column_metadata_schemas": column_metadata_schemas(), + "generated_column_schemas": generated_column_schemas(), + }, + indent=2, + ) + + "\n" + ) + + +def emit(out: TextIO) -> None: + """Write the corpus through the normal multi-repository generation driver.""" + out.write(render()) diff --git a/vgi/codegen/cpp_protocol_name.py b/vgi/codegen/cpp_protocol_name.py index 8c2b1d0a..fa6851bd 100644 --- a/vgi/codegen/cpp_protocol_name.py +++ b/vgi/codegen/cpp_protocol_name.py @@ -17,7 +17,7 @@ transports made ``vgi_rpc.protocol`` a required routing key, at which point there was no single string the DuckDB extension could send. -The names were then decided (``vgi.v2``, ``vgi.secret.v1``) and hand-written on +The names were originally decided (``vgi.v2``, ``vgi.secret.v1``) and hand-written on both sides. That is the same shape of hazard one step removed: the version is generated *and* drift-guarded, so a version bump that misses the C++ tree is a red build, while a rename that missed it was a silent misroute. A wire contract diff --git a/vgi/codegen/cpp_records.py b/vgi/codegen/cpp_records.py index 2bb7bd98..6b6d1281 100644 --- a/vgi/codegen/cpp_records.py +++ b/vgi/codegen/cpp_records.py @@ -617,14 +617,21 @@ def _factory_names() -> set[str]: _REGEN = ["uv run --project ~/Development/vgi-python python scripts/regen_generated.py"] -def emit(out: TextIO, record: str = "hpp", namespace: list[str] | None = None) -> None: +def emit(out: TextIO, record: str = "hpp", namespace: list[str] | None = None, backend: str = "arrow") -> None: """Emit the records header (``record="hpp"``) or source (``record="cpp"``) to *out*.""" ns = namespace if namespace is not None else parse_cpp_namespace(DEFAULT_CPP_NAMESPACE) model = build_model() + if backend == "nanoarrow": + from vgi.codegen.cpp_records_nano import _render_header as render_header + from vgi.codegen.cpp_records_nano import _render_source as render_source + elif backend == "arrow": + render_header, render_source = _render_header, _render_source + else: + raise GeneratorError(f"unknown C++ record backend {backend!r}") if record == "hpp": - body = _render_header(model, ns) + body = render_header(model, ns) elif record == "cpp": - body = _render_source(model, ns) + body = render_source(model, ns) else: raise GeneratorError(f"cpp_records: unknown output {record!r} (expected 'hpp' or 'cpp').") out.write( @@ -632,7 +639,9 @@ def emit(out: TextIO, record: str = "hpp", namespace: list[str] | None = None) - generator_module="vgi.codegen.cpp_records", generator_command="python -m vgi.codegen.cpp_records", generator_version=GENERATOR_VERSION, - regen_command_lines=_REGEN, + regen_command_lines=[line + " --native-cpp-backend nanoarrow" for line in _REGEN] + if backend == "nanoarrow" + else _REGEN, body=body, ) ) diff --git a/vgi/codegen/cpp_records_nano.py b/vgi/codegen/cpp_records_nano.py new file mode 100644 index 00000000..ea66dc7f --- /dev/null +++ b/vgi/codegen/cpp_records_nano.py @@ -0,0 +1,210 @@ +# Copyright 2026 Query Farm LLC - https://query.farm +"""Nanoarrow rendering for the shared native protocol-record model. + +Only runtime syntax differs; types, fields, presence and wire identities come +from cpp_records.build_model, shared with the Arrow C++ renderer. +""" + +from __future__ import annotations + +import io + +from vgi.codegen._common import close_namespace, open_namespace +from vgi.codegen.cpp_records import HEADER_NAME, _dependency_order, _doc, _factory_names, _method_rows, _Model +from vgi.codegen.cpp_schemas import _na_emit_field + + +def _render_header(model: _Model, namespace: list[str]) -> str: + out = io.StringIO() + out.write("#pragma once\n\n") + out.write("// Typed VGI protocol records. Encode/decode with ToBatch()/FromBatch()\n") + out.write("// (vgi_protocol_records.cpp), over the nanoarrow-backed types of vgi_arrow.hpp.\n\n") + out.write('#include "vgi_arrow.hpp"\n\n') + for inc in ("cstdint", "map", "memory", "optional", "string", "utility", "vector"): + out.write(f"#include <{inc}>\n") + out.write("\n") + out.write(open_namespace(namespace)) + out.write("\n") + out.write( + "using ::duckdb::vgi::VgiBatchPtr;\nusing ::duckdb::vgi::VgiBufferPtr;\nusing ::duckdb::vgi::VgiSchemaPtr;\n\n" + ) + out.write("using Bytes = std::vector;\n") + out.write("//! A large_binary value, shared rather than copied (filter and join-key payloads can be large).\n") + out.write("using LargeBytes = VgiBufferPtr;\n") + out.write("//! timestamp[us, tz=UTC]: microseconds since the Unix epoch.\n") + out.write("using TimestampMicros = int64_t;\n\n") + + out.write("// " + "-" * 76 + "\n// Enums. Enumerator order is the wire dictionary order.\n// " + "-" * 76 + "\n\n") + for e in model.enums.values(): + out.write("\n".join(_doc(e.doc, "")) + ("\n" if e.doc else "")) + out.write(f"enum class {e.name} : int16_t {{\n") + for em in e.members: + out.write(f"\t{em.ident},\n") + out.write("};\n") + out.write(f"//! The wire name of a {e.name} value.\n") + out.write(f"const char *ToWire({e.name} value);\n") + out.write(f"//! The {e.name} with this wire name, or std::nullopt.\n") + out.write(f"std::optional<{e.name}> {e.name}FromWire(const std::string &wire);\n\n") + + out.write("// " + "-" * 76 + "\n// Records.\n// " + "-" * 76 + "\n\n") + # Forward-declare so a record can hold one declared later. + for r in model.records.values(): + out.write(f"struct {r.name};\n") + out.write("\n") + for r in _dependency_order(model): + out.write("\n".join(_doc(r.doc, "")) + ("\n" if r.doc else "")) + out.write(f"//! Protocol type: {', '.join(sorted(set(r.origins)))}.\n") + out.write(f"struct {r.name} {{\n") + for m in r.members: + for line in _doc(m.doc, "\t"): + out.write(line + "\n") + init = f" = {m.initializer}" if m.initializer else "{}" + out.write(f"\t{m.declared_type} {m.ident}{init};\n") + out.write("};\n") + out.write(f"bool operator==(const {r.name} &a, const {r.name} &b);\n") + out.write(f"inline bool operator!=(const {r.name} &a, const {r.name} &b) {{\n\treturn !(a == b);\n}}\n") + out.write(f"//! A one-row batch with {r.name}'s exact protocol schema.\n") + out.write(f"VgiBatchPtr ToBatch(const {r.name} &value);\n") + out.write( + f"//! Decode row `row` of `batch` (protocol errors name `worker_path`).\n" + f"void FromBatch(const VgiBatchPtr &batch, int64_t row, {r.name} &out, const std::string &worker_path);\n\n" + ) + + out.write("// " + "-" * 76 + "\n// Methods.\n// " + "-" * 76 + "\n\n") + out.write("//! One VgiProtocol method's wire schemas (vgi_protocol_schemas.hpp factories).\n") + out.write("struct MethodSchemas {\n") + out.write("\tconst char *method;\n") + out.write("\t//! The outer request batch (`ParamsSchema`).\n") + out.write("\tVgiSchemaPtr params;\n") + out.write("\t//! The record a unary result carries (`ResultSchema`); null when the method\n") + out.write("\t//! returns nothing, raw bytes, or is a stream.\n") + out.write("\tVgiSchemaPtr result;\n") + out.write("\t//! False for a unary method that returns None.\n") + out.write("\tbool returns_value;\n") + out.write("};\n") + out.write("//! Every VgiProtocol method, by name order.\n") + out.write("const std::vector &AllMethodSchemas();\n\n") + out.write("// Test hooks (test/cpp/test_protocol_records.cpp).\n") + out.write("//! Every record's name with a one-row batch of a default-constructed instance.\n") + out.write("std::vector> DefaultRecordBatchesForTesting();\n") + out.write("//! Records that have a vgi_protocol_schemas.hpp factory: name, record schema, factory schema.\n") + out.write("struct RecordSchemaPair {\n\tstd::string name;\n\tVgiSchemaPtr record;\n") + out.write("\tVgiSchemaPtr factory;\n};\n") + out.write("std::vector RecordSchemaPairsForTesting();\n\n") + out.write("//! Decode row `row` of `batch` as a T (any record above).\n") + out.write("template \n") + out.write("T Decode(const VgiBatchPtr &batch, int64_t row, const std::string &worker_path) {\n") + out.write("\tT out;\n\tFromBatch(batch, row, out, worker_path);\n\treturn out;\n}\n\n") + out.write(close_namespace(namespace)) + return out.getvalue() + + +def _render_source(model: _Model, namespace: list[str]) -> str: + out = io.StringIO() + out.write(f'#include "{HEADER_NAME}"\n\n') + out.write('#include "vgi_protocol_schemas.hpp"\n') + out.write('#include "vgi_secret_protocol_schemas.hpp"\n') + out.write('#include "vgi_wire_codec.hpp"\n\n') + out.write(open_namespace(namespace)) + out.write("\nnamespace wire = ::duckdb::vgi::wire;\n") + out.write( + "using ::duckdb::vgi::MakeSchema;\nusing ::duckdb::vgi::VgiBatchBuilder;\nusing ::duckdb::vgi::VgiColumn;\n" + ) + out.write("namespace type = ::duckdb::vgi::type;\n") + out.write("// The codec's overloads join the generated ones (records and enums find each other by ADL).\n") + out.write("using wire::ReadValue;\nusing wire::WriteValue;\n\n") + + # Enums. + for e in model.enums.values(): + wires = ", ".join(f'"{m.wire_name}"' for m in e.members) + out.write(f"static const char *const k{e.name}Wire[] = {{{wires}}};\n") + out.write(f"static constexpr int16_t k{e.name}Count = {len(e.members)};\n\n") + out.write(f"const char *ToWire({e.name} value) {{\n") + out.write("\tauto i = static_cast(value);\n") + out.write(f'\treturn i >= 0 && i < k{e.name}Count ? k{e.name}Wire[i] : "";\n}}\n\n') + out.write(f"std::optional<{e.name}> {e.name}FromWire(const std::string &wire) {{\n") + out.write(f"\tfor (int16_t i = 0; i < k{e.name}Count; i++) {{\n") + out.write(f"\t\tif (wire == k{e.name}Wire[i]) {{\n\t\t\treturn static_cast<{e.name}>(i);\n\t\t}}\n\t}}\n") + out.write("\treturn std::nullopt;\n}\n\n") + out.write(f"void ReadValue(const VgiColumn &col, int64_t i, {e.name} &out, const wire::Ctx &ctx) {{\n") + out.write("\tstd::string s;\n\twire::ReadValue(col, i, s, ctx);\n") + out.write(f"\tauto v = {e.name}FromWire(s);\n") + out.write(f'\tif (!v) {{\n\t\twire::ThrowBadEnum(ctx, "{e.name}", s);\n\t}}\n') + out.write("\tout = *v;\n}\n\n") + out.write(f"void WriteValue(ArrowArray *array, {e.name} value) {{\n") + out.write(f"\twire::WriteEnum(array, static_cast(value), k{e.name}Wire, k{e.name}Count);\n}}\n\n") + + # Records: schema factories, nested-struct codecs (declared first so any order works), batch codecs. + out.write("// Nested-struct codecs, declared up front so records can nest in any order.\n") + for r in model.records.values(): + out.write(f"void ReadValue(const VgiColumn &col, int64_t i, {r.name} &out, const wire::Ctx &ctx);\n") + out.write(f"void WriteValue(ArrowArray *array, const {r.name} &value);\n") + out.write("\n") + + for r in model.records.values(): + fields = ",\n\t ".join(_na_emit_field(f, origin=f"{r.name}.{f.name}") for f in r.schema) + out.write(f"static const VgiSchemaPtr &{r.name}RecordSchema() {{\n") + out.write( + f"\tstatic const auto schema = MakeSchema({{\n\t {fields},\n\t}});\n" + if r.members + else "\tstatic const auto schema = MakeSchema({});\n" + ) + out.write("\treturn schema;\n}\n\n") + + out.write(f"bool operator==(const {r.name} &a, const {r.name} &b) {{\n") + if r.members: + conds = " &&\n\t ".join(f"wire::Equal(a.{m.ident}, b.{m.ident})" for m in r.members) + out.write(f"\treturn {conds};\n}}\n\n") + else: + out.write("\t(void)a;\n\t(void)b;\n\treturn true;\n}\n\n") + + out.write(f"template \nstatic void Read{r.name}(Reader &r, {r.name} &out) {{\n") + if not r.members: + out.write("\t(void)r;\n\t(void)out;\n") + for m in r.members: + out.write(f'\tr.{m.presence.value}("{m.wire_name}", out.{m.ident});\n') + out.write("}\n\n") + + out.write(f"void ReadValue(const VgiColumn &col, int64_t i, {r.name} &out, const wire::Ctx &ctx) {{\n") + out.write(f'\twire::StructReader r(col, i, ctx, "{r.name}");\n\tRead{r.name}(r, out);\n}}\n\n') + out.write(f"void WriteValue(ArrowArray *array, const {r.name} &value) {{\n") + if not r.members: + out.write("\t(void)value;\n") + for idx, m in enumerate(r.members): + out.write(f"\tWriteValue(array->children[{idx}], value.{m.ident});\n") + out.write("\t::duckdb::vgi::append::FinishElement(array);\n}\n\n") + + out.write(f"VgiBatchPtr ToBatch(const {r.name} &value) {{\n") + out.write(f"\tVgiBatchBuilder b({r.name}RecordSchema());\n") + if not r.members: + out.write("\t(void)value;\n") + for idx, m in enumerate(r.members): + out.write(f"\tWriteValue(b.column({idx}), value.{m.ident});\n") + out.write("\tb.FinishRow();\n\treturn b.Finish();\n}\n\n") + out.write( + f"void FromBatch(const VgiBatchPtr &batch, int64_t row, {r.name} &out, const std::string &worker_path) {{\n" + ) + out.write(f'\twire::RowReader r(batch, row, "{r.name}", worker_path);\n') + out.write("\twire::CurrentBatchScope scope(batch.get());\n") + out.write(f"\tRead{r.name}(r, out);\n}}\n\n") + + out.write("const std::vector &AllMethodSchemas() {\n") + out.write("\tstatic const std::vector methods = {\n") + for method, params, result, returns in _method_rows(): + result_expr = f"{result}()" if result else "nullptr" + out.write(f'\t {{"{method}", {params}(), {result_expr}, {"true" if returns else "false"}}},\n') + out.write("\t};\n\treturn methods;\n}\n\n") + + factories = _factory_names() + out.write("std::vector> DefaultRecordBatchesForTesting() {\n") + out.write("\treturn {\n") + for r in model.records.values(): + out.write(f'\t {{"{r.name}", ToBatch({r.name} {{}})}},\n') + out.write("\t};\n}\n\n") + out.write("std::vector RecordSchemaPairsForTesting() {\n\treturn {\n") + for r in model.records.values(): + if r.name in factories: + out.write(f'\t {{"{r.name}", {r.name}RecordSchema(), {r.name}Schema()}},\n') + out.write("\t};\n}\n\n") + out.write(close_namespace(namespace)) + return out.getvalue() diff --git a/vgi/codegen/cpp_registry.py b/vgi/codegen/cpp_registry.py index 79eea87a..ee9cc4b6 100644 --- a/vgi/codegen/cpp_registry.py +++ b/vgi/codegen/cpp_registry.py @@ -1,6 +1,6 @@ # Copyright 2025, 2026 Query Farm LLC - https://query.farm -r"""Emit vgi-c++'s ``vgi_service.hpp``: the whole ``vgi.v2`` registry, for the C++ SDK. +r"""Emit vgi-c++'s ``vgi_service.hpp``: the whole ``vgi.v3`` registry, for the C++ SDK. vgi-rpc-c++ does not reflect over anything. A method is registered by calling ``ServerBuilder::add_unary`` / ``add_void`` / ``add_exchange`` with its name, its @@ -8,7 +8,7 @@ this port needs both halves of the registry generated (see :mod:`vgi.codegen._registry` for the model and the reasons): -- ``class VgiService``: one ``virtual`` member per ``vgi.v2`` method, every one +- ``class VgiService``: one ``virtual`` member per ``vgi.v3`` method, every one taking ``(const vgi_rpc::Request&, vgi_rpc::CallContext&)`` and returning ``vgi_rpc::Result`` (a unary with a return), ``void`` (a unary without one) or ``vgi_rpc::Stream`` (a stream). Each default body throws @@ -77,7 +77,7 @@ TARGET = "src/generated/vgi_service.hpp" #: The schema factory for each ``result`` column a unary may declare, by -#: nullability. Every vgi.v2 result is one ``binary`` column (a record's IPC +#: nullability. Every vgi.v3 result is one ``binary`` column (a record's IPC #: bytes, or raw bytes); a result of any other type is a protocol change this #: generator has to learn about, not one it should guess at. _RESULT_ENVELOPES: dict[bool, str] = { @@ -134,7 +134,7 @@ def _row(m: RegistryMethod, available: set[str]) -> list[str]: if stems.header is not None: header = _factory(sanitize_name(stems.header), available, m.name) if m.is_stream and header is None: - raise GeneratorError(f"{m.name}: a vgi.v2 stream without a header") + raise GeneratorError(f"{m.name}: a vgi.v3 stream without a header") fields = [f'.name = "{m.name}"', f".params = {params}"] if result is not None: fields.append(f".result = {result}") @@ -170,9 +170,9 @@ def _envelope(name: str, nullable: bool) -> str: _PREAMBLE = """\ -// The vgi.v2 method registry. +// The vgi.v3 method registry. // -// Every vgi.v2 method is registered, with exactly the reference's method type +// Every vgi.v3 method is registered, with exactly the reference's method type // and params / result / header schemas, so vgi_rpc.Reflection.v1 reports the // reference's protocol hash. A method this worker does not override answers // UNIMPLEMENTED (kind `method_not_implemented`). @@ -187,7 +187,7 @@ def _envelope(name: str, nullable: bool) -> str: // the IPC bytes of a one-row batch in `payload` -- or, when `payload` is null, // raw bytes. A handler builds its answer in `payload`, never in `result`. -/// What a vgi.v2 method a worker does not implement answers: code +/// What a vgi.v3 method a worker does not implement answers: code /// UNIMPLEMENTED, kind `method_not_implemented`, " is not implemented by /// this worker". The body of every `VgiService` default. [[noreturn]] inline void method_not_implemented(std::string_view method) { @@ -220,7 +220,7 @@ def _render_body(methods: Sequence[RegistryMethod], namespace: list[str]) -> str body.write(_envelope(name, nullable)) body.write("\n") - body.write("/// Every vgi.v2 method, each defaulting to `method_not_implemented`.\n") + body.write("/// Every vgi.v3 method, each defaulting to `method_not_implemented`.\n") body.write("class VgiService {\n") body.write("public:\n") body.write("\tvirtual ~VgiService() = default;\n") @@ -255,7 +255,7 @@ def _render_body(methods: Sequence[RegistryMethod], namespace: list[str]) -> str body.write("\tstd::string_view doc;\n") body.write("};\n\n") - body.write("/// Every vgi.v2 method, by wire name.\n") + body.write("/// Every vgi.v3 method, by wire name.\n") body.write(f"inline constexpr std::array VGI_METHODS = {{{{\n") for m in methods: for line in _row(m, available): diff --git a/vgi/codegen/cpp_request_builders.py b/vgi/codegen/cpp_request_builders.py index db6664a9..5f40d56d 100644 --- a/vgi/codegen/cpp_request_builders.py +++ b/vgi/codegen/cpp_request_builders.py @@ -121,13 +121,20 @@ def _is_simple_map(dtype: pa.DataType) -> bool: def _is_supported_field(field: pa.Field[Any]) -> bool: - return _is_simple_scalar(field.type) or _is_simple_list(field.type) or _is_simple_map(field.type) + return ( + _is_simple_scalar(field.type) + or _is_simple_list(field.type) + or _is_simple_map(field.type) + or pa.types.is_struct(field.type) + ) -def _cpp_param_type(field: pa.Field[Any]) -> str: +def _cpp_param_type(field: pa.Field[Any], backend: str = "arrow") -> str: """C++ parameter type for a schema field (with std::optional for nullable).""" base: str t = field.type + if pa.types.is_struct(t): + return "const VgiColumn&" if backend == "nanoarrow" else "const std::shared_ptr&" if t.equals(pa.binary()): base = "std::vector" elif t.equals(pa.string()): @@ -261,6 +268,8 @@ def _build_array_call(field: pa.Field[Any], param: str, *, method_name: str) -> if _is_simple_list(t): inner = t.value_type if inner.equals(pa.string()): + if nullable: + return f"arrays.push_back(BuildOptionalStringListScalar({param}));" return f"arrays.push_back(BuildStringListScalar({param}));" if inner.equals(pa.binary()): return f"arrays.push_back(BuildBinaryListScalar({param}));" @@ -283,6 +292,7 @@ def _emit_builder( *, method_name: str, exception_type: str = "IOException", + backend: str = "arrow", ) -> str: """Render one ``BuildParams(...)`` function as an inline C++ definition.""" schema = es.schema @@ -301,24 +311,57 @@ def _emit_builder( # Param signatures — one per field, in schema order. params_decl: list[str] = [] for f in schema: - cpp_type = _cpp_param_type(f) + cpp_type = _cpp_param_type(f, backend) params_decl.append(f"\t {cpp_type} {f.name}") params_str = ",\n".join(params_decl) # Body — emit one push_back per field. pushes: list[str] = [] - for f in schema: - pushes.append(f"\t{_build_array_call(f, f.name, method_name=method_name)}") + for i, f in enumerate(schema): + if pa.types.is_struct(f.type) and backend == "nanoarrow": + nullable = "true" if f.nullable else "false" + pushes.append(f"\tarrays.push_back(BuildStructScalar({f.name}, schema->field({i}), {nullable}));") + elif pa.types.is_struct(f.type): + param = f.name + pushes.extend( + [ + f"\tif ({param}) {{", + f"\t\tif ({param}->length() != 1 || !{param}->type()->Equals(schema->field({i})->type())" + + (f" || {param}->IsNull(0)" if not f.nullable else "") + + ") {", + f'\t\t\tthrow {exception_type}("{func_name}: invalid {param} struct");', + "\t\t}", + f"\t\tarrays.push_back({param});", + "\t} else {", + ] + ) + if f.nullable: + pushes.extend( + [ + f"\t\tauto null_array = arrow::MakeArrayOfNull(schema->field({i})->type(), 1);", + f"\t\tif (!null_array.ok()) throw {exception_type}(null_array.status().ToString());", + "\t\tarrays.push_back(null_array.ValueOrDie());", + ] + ) + else: + pushes.append(f'\t\tthrow {exception_type}("{func_name}: missing {param} struct");') + pushes.append("\t}") + else: + pushes.append(f"\t{_build_array_call(f, f.name, method_name=method_name)}") body = "\n".join(pushes) out = io.StringIO() out.write(f"// Origin: {es.origin}\n") - out.write(f"inline std::shared_ptr {func_name}(\n") + batch_type = "VgiBatchPtr" if backend == "nanoarrow" else "std::shared_ptr" + out.write(f"inline {batch_type} {func_name}(\n") out.write(params_str) out.write(") {\n") out.write(f"\tconst auto &schema = {name}Schema();\n") out.write('\tstatic_assert(true, "compile-time anchor — runtime field-count assert below");\n') - out.write("\tstd::vector> arrays;\n") + array_type = "ArrowArray" if backend == "nanoarrow" else "std::shared_ptr" + out.write(f"\tstd::vector<{array_type}> arrays;\n") + if backend == "nanoarrow": + out.write("\tRequestArrayGuard guard(arrays);\n") out.write(f"\tarrays.reserve({len(schema)});\n") out.write(body + "\n") out.write("\tif (arrays.size() != static_cast(schema->num_fields())) {\n") @@ -328,7 +371,10 @@ def _emit_builder( f'+ std::to_string(schema->num_fields()) + " fields");\n' ) out.write("\t}\n") - out.write("\treturn arrow::RecordBatch::Make(schema, 1, arrays);\n") + if backend == "nanoarrow": + out.write("\treturn VgiBatch::FromColumns(schema, 1, std::move(arrays));\n") + else: + out.write("\treturn arrow::RecordBatch::Make(schema, 1, arrays);\n") out.write("}\n") return out.getvalue() @@ -365,6 +411,7 @@ def emit_builders( exception_include: str = '"duckdb/common/exception.hpp"', exception_type: str = "IOException", standalone_schemas: tuple[EmittedSchema, ...] = (), + backend: str = "arrow", ) -> None: """Render request-builder functions for a set of params schemas as a C++ header. @@ -392,6 +439,10 @@ def emit_builders( existing callers (and the drift tests that compare against ``vgi``'s checked-in copy) are unaffected. """ + if backend == "nanoarrow": + regen_command_lines = [ + "uv run --project ~/Development/vgi-python python scripts/regen_generated.py --native-cpp-backend nanoarrow" + ] if namespace is None: namespace = parse_cpp_namespace(DEFAULT_CPP_NAMESPACE) body = io.StringIO() @@ -405,7 +456,10 @@ def emit_builders( body.write("#include \n") body.write("#include \n") body.write("#include \n\n") - body.write("#include \n\n") + if backend not in ("arrow", "nanoarrow"): + raise GeneratorError(f"unknown C++ builder backend {backend!r}") + if backend == "arrow": + body.write("#include \n\n") body.write(f"#include {exception_include}\n\n") body.write(open_namespace(namespace)) body.write("\n") @@ -425,7 +479,7 @@ def emit_builders( ) skipped.append(method_name) continue - chunk = _emit_builder(es, method_name=method_name, exception_type=exception_type) + chunk = _emit_builder(es, method_name=method_name, exception_type=exception_type, backend=backend) body.write(chunk + "\n") if chunk.lstrip().startswith("// SKIPPED"): skipped.append(method_name) @@ -433,7 +487,7 @@ def emit_builders( emitted.append(method_name) for es in standalone_schemas: - chunk = _emit_builder(es, method_name=es.name, exception_type=exception_type) + chunk = _emit_builder(es, method_name=es.name, exception_type=exception_type, backend=backend) body.write(chunk + "\n") if chunk.lstrip().startswith("// SKIPPED"): skipped.append(es.name) @@ -474,6 +528,7 @@ def emit( helpers_include: str = "vgi_rpc_types.hpp", exception_include: str = '"duckdb/common/exception.hpp"', exception_type: str = "IOException", + backend: str = "arrow", ) -> None: """Emit the generated C++ request-builder header to *out*.""" standalone_schemas = tuple( @@ -491,6 +546,7 @@ def emit( ], schemas_include=schemas_include, namespace=namespace, + backend=backend, helpers_include=helpers_include, exception_include=exception_include, exception_type=exception_type, diff --git a/vgi/codegen/cpp_schemas.py b/vgi/codegen/cpp_schemas.py index a678216e..bd445a79 100644 --- a/vgi/codegen/cpp_schemas.py +++ b/vgi/codegen/cpp_schemas.py @@ -175,6 +175,98 @@ def _emit_field(field: pa.Field[Any], *, origin: str) -> str: return f'arrow::field("{field.name}", {type_expr}, /*nullable=*/{nullable})' +# --------------------------------------------------------------------------- # +# nanoarrow backend: pyarrow DataType -> vgi::type DSL (vgi_arrow.hpp) +# --------------------------------------------------------------------------- # + +_NA_SCALAR_MAP: dict[Any, str] = { + pa.null(): "type::Null()", + pa.bool_(): "type::Bool()", + pa.int8(): "type::Int8()", + pa.int16(): "type::Int16()", + pa.int32(): "type::Int32()", + pa.int64(): "type::Int64()", + pa.uint8(): "type::UInt8()", + pa.uint16(): "type::UInt16()", + pa.uint32(): "type::UInt32()", + pa.uint64(): "type::UInt64()", + pa.float32(): "type::Float32()", + pa.float64(): "type::Float64()", + pa.string(): "type::Utf8()", + pa.binary(): "type::Binary()", + pa.large_string(): "type::LargeUtf8()", + pa.large_binary(): "type::LargeBinary()", +} + + +def _na_emit_type(dtype: pa.DataType, *, origin: str) -> str: + """Render a pyarrow DataType as a ``vgi::type`` expression (the nanoarrow backend).""" + for proto, expr in _NA_SCALAR_MAP.items(): + if dtype.equals(proto): + return expr + if pa.types.is_list(dtype) or pa.types.is_large_list(dtype): + value_field = dtype.value_field + if value_field.name != "item": + raise GeneratorError(f"list at {origin} names its item {value_field.name!r}; only 'item' is supported") + inner = _na_emit_type(value_field.type, origin=f"{origin}[list item]") + ctor = "type::LargeList" if pa.types.is_large_list(dtype) else "type::List" + nullable = "true" if value_field.nullable else "false" + return f"{ctor}({inner}, /*item_nullable=*/{nullable})" + if pa.types.is_map(dtype): + key_field = dtype.key_field + item_field = dtype.item_field + if key_field.name != "key" or item_field.name != "value" or key_field.nullable is not False: + raise GeneratorError(f"Map at {origin} uses non-default child fields") + if not _uses_default_map_field_name(dtype): + raise GeneratorError(f"Map at {origin} uses a non-default entries field") + key = _na_emit_type(dtype.key_type, origin=f"{origin}[map key]") + item = _na_emit_type(dtype.item_type, origin=f"{origin}[map value]") + nullable = "true" if item_field.nullable else "false" + return f"type::Map({key}, {item}, /*value_nullable=*/{nullable})" + if pa.types.is_dictionary(dtype): + if dtype.ordered: + raise GeneratorError(f"ordered dictionary at {origin} is not supported") + index = _na_emit_type(dtype.index_type, origin=f"{origin}[dict index]") + value = _na_emit_type(dtype.value_type, origin=f"{origin}[dict value]") + return f"type::Dictionary({index}, {value})" + if pa.types.is_struct(dtype): + children = [ + _na_emit_field(dtype.field(i), origin=f"{origin}[struct child {i}]") for i in range(dtype.num_fields) + ] + return "type::Struct({" + ", ".join(children) + "})" + if pa.types.is_timestamp(dtype): + units = {"s": "s", "ms": "m", "us": "u", "ns": "n"} + if dtype.unit not in units: + raise GeneratorError(f"timestamp at {origin} has unknown unit {dtype.unit!r}") + tz = dtype.tz or "" + return f"type::Timestamp('{units[dtype.unit]}', \"{tz}\")" + raise GeneratorError(f"vgi.codegen.cpp_schemas (nanoarrow): unsupported Arrow type {dtype!r} at {origin}") + + +def _na_emit_field(field: pa.Field[Any], *, origin: str) -> str: + """Render a pyarrow Field as a named ``vgi::VgiField`` expression.""" + if field.metadata: + raise GeneratorError(f"field metadata at {origin} is not supported by the nanoarrow backend") + type_expr = _na_emit_type(field.type, origin=f"{origin}[{field.name}]") + nullable = "true" if field.nullable else "false" + return f'{type_expr}.Named("{field.name}", /*nullable=*/{nullable})' + + +def _na_emit_factory(es: EmittedSchema) -> str: + if es.schema.metadata: + raise GeneratorError(f"schema metadata on {es.name} is not supported by the nanoarrow backend") + body = f"// Origin: {es.origin}\ninline const VgiSchemaPtr &{es.name}Schema() {{\n" + if len(es.schema) == 0: + body += "\tstatic const auto schema = MakeSchema({});\n" + else: + body += "\tstatic const auto schema = MakeSchema({\n" + lines = ["\t " + _na_emit_field(f, origin=f"{es.name}.{f.name}") for f in es.schema] + body += ",\n".join(lines) + ",\n" + body += "\t});\n" + body += "\treturn schema;\n}\n" + return body + + def _emit_factory(es: EmittedSchema) -> str: body = f"// Origin: {es.origin}\ninline const std::shared_ptr &{es.name}Schema() {{\n" if len(es.schema) == 0: @@ -206,23 +298,42 @@ def emit_schemas( generator_command: str, regen_command_lines: list[str], namespace: list[str] | None = None, + backend: str = "arrow", ) -> None: """Render a list of EmittedSchema records as a C++ schema-factory header. + ``backend`` is ``"arrow"`` (Arrow C++ factories; vgi-c++, vgi-sqlite) or + ``"nanoarrow"`` (``VgiSchemaPtr`` factories over ``vgi_arrow.hpp``; the DuckDB + extension). + Shared by the main protocol generator and the secret protocol generator; only the schema set and the provenance banner differ. """ + if backend == "nanoarrow": + regen_command_lines = [ + "uv run --project ~/Development/vgi-python python scripts/regen_generated.py --native-cpp-backend nanoarrow" + ] if namespace is None: namespace = parse_cpp_namespace(DEFAULT_CPP_NAMESPACE) body = io.StringIO() + if backend not in ("arrow", "nanoarrow"): + raise GeneratorError(f"unknown C++ schema backend {backend!r}") body.write("#pragma once\n\n") - body.write("#include \n") + if backend == "arrow": + body.write("#include \n") + else: + body.write('#include "vgi_arrow.hpp"\n') body.write("#include \n\n") body.write(open_namespace(namespace)) body.write("\n") + if backend == "nanoarrow": + body.write("using ::duckdb::vgi::MakeSchema;\n") + body.write("using ::duckdb::vgi::VgiSchemaPtr;\n") + body.write("namespace type = ::duckdb::vgi::type;\n\n") + factory = _emit_factory if backend == "arrow" else _na_emit_factory for es in schemas: - body.write(_emit_factory(es)) + body.write(factory(es)) body.write("\n") body.write(close_namespace(namespace)) @@ -242,8 +353,8 @@ def emit_schemas( out.write(body.getvalue()) -def emit(out: TextIO, namespace: list[str] | None = None) -> None: - """Emit the generated C++ schemas header to *out*.""" +def emit(out: TextIO, namespace: list[str] | None = None, backend: str = "arrow") -> None: + """Emit the generated C++ schemas header to *out* (``backend="nanoarrow"``: the nanoarrow backend).""" emit_schemas( out, # The request records are included so the C++ client can be CHECKED @@ -259,6 +370,7 @@ def emit(out: TextIO, namespace: list[str] | None = None) -> None: " > ~/Development/vgi/src/generated/vgi_protocol_schemas.hpp", ], namespace=namespace, + backend=backend, ) diff --git a/vgi/codegen/cpp_sdk.py b/vgi/codegen/cpp_sdk.py new file mode 100644 index 00000000..1b56e3bf --- /dev/null +++ b/vgi/codegen/cpp_sdk.py @@ -0,0 +1,34 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Generate the C++ worker SDK's public headers with its own namespace.""" + +from __future__ import annotations + +import importlib +import io +import re +from typing import TextIO + +_GENERATORS = { + "schemas": "cpp_schemas", + "constants": "cpp_constants", + "version": "cpp_protocol_version", + "names": "cpp_protocol_name", +} + + +def emit(out: TextIO, *, record: str) -> None: + """Emit a public header through the coordinated, atomic generation driver.""" + module = importlib.import_module(f"vgi.codegen.{_GENERATORS[record]}") + rendered = io.StringIO() + module.emit(rendered, namespace=["vgi", "generated"]) + # Keep the SDK's regeneration command and avoid backslash-continued C++ + # comments, which GCC diagnoses even when Clang accepts them. + out.write( + re.sub( + r"// To regenerate:\n(// [^\n]*\n)+", + "// To regenerate:\n// scripts/regenerate_protocol.sh\n", + rendered.getvalue(), + count=1, + ) + ) diff --git a/vgi/codegen/cpp_secret_request_builders.py b/vgi/codegen/cpp_secret_request_builders.py index bfba01e5..05379644 100644 --- a/vgi/codegen/cpp_secret_request_builders.py +++ b/vgi/codegen/cpp_secret_request_builders.py @@ -28,8 +28,8 @@ from typing import TextIO -def emit(out: TextIO, namespace: list[str] | None = None) -> None: - """Emit the generated C++ secret request-builder header to *out*.""" +def emit(out: TextIO, namespace: list[str] | None = None, backend: str = "arrow") -> None: + """Emit the generated C++ secret request-builder header to *out* (``backend="nanoarrow"``: nanoarrow).""" schemas = collect_schemas( VgiSecretProtocol, info_types=(), @@ -46,6 +46,8 @@ def emit(out: TextIO, namespace: list[str] | None = None) -> None: " > ~/Development/vgi/src/generated/vgi_secret_request_builders.hpp", ], schemas_include="vgi_secret_protocol_schemas.hpp", + namespace=namespace, + backend=backend, ) diff --git a/vgi/codegen/cpp_secret_schemas.py b/vgi/codegen/cpp_secret_schemas.py index c53be6cf..6b085528 100644 --- a/vgi/codegen/cpp_secret_schemas.py +++ b/vgi/codegen/cpp_secret_schemas.py @@ -30,8 +30,8 @@ from typing import TextIO -def emit(out: TextIO, namespace: list[str] | None = None) -> None: - """Emit the generated C++ secret-schema header to *out*.""" +def emit(out: TextIO, namespace: list[str] | None = None, backend: str = "arrow") -> None: + """Emit the generated C++ secret-schema header to *out* (``backend="nanoarrow"``: the nanoarrow backend).""" schemas = collect_schemas( VgiSecretProtocol, info_types=(), @@ -48,6 +48,7 @@ def emit(out: TextIO, namespace: list[str] | None = None) -> None: " > ~/Development/vgi/src/generated/vgi_secret_protocol_schemas.hpp", ], namespace=namespace, + backend=backend, ) diff --git a/vgi/codegen/csharp_registry.py b/vgi/codegen/csharp_registry.py index 90637d54..f890f9d8 100644 --- a/vgi/codegen/csharp_registry.py +++ b/vgi/codegen/csharp_registry.py @@ -1,6 +1,6 @@ # Copyright 2025, 2026 Query Farm LLC - https://query.farm -r"""Emit vgi-csharp's ``IVgiService``: the whole ``vgi.v2`` registry, for vgi-csharp. +r"""Emit vgi-csharp's ``IVgiService``: the whole ``vgi.v3`` registry, for vgi-csharp. vgi-rpc-csharp registers a protocol by reflecting over a C# interface: every method is a wire method (``BindAsync`` is ``bind``), its parameters derive the @@ -11,7 +11,7 @@ module generates it from :class:`vgi.protocol.VgiProtocol` (see :mod:`vgi.codegen._registry` for the model and the reasons): -- every ``vgi.v2`` method, typed with the records and enums +- every ``vgi.v3`` method, typed with the records and enums :mod:`vgi.codegen.csharp_types` already generates, so the derivation reproduces the reference schemas exactly; - a default interface method body on every one that throws vgi-csharp's @@ -41,6 +41,7 @@ from __future__ import annotations import enum +import json import re from collections.abc import Sequence from dataclasses import dataclass @@ -77,6 +78,10 @@ #: The C# type a stream method returns (inside its ``Task``). CSHARP_STREAM_TYPE = "RpcStream" +#: Preserve canonical projection-envelope bytes until the service validates its +#: exact schema, single row/batch and complete framing. The wire stays binary. +CSHARP_RAW_PARAMS = {("table_function_projection", "request")} + _CTX = "ICallContext? ctx = null" _GENERIC_NS = "global::System.Collections.Generic." @@ -122,6 +127,8 @@ def _default(p: RegistryParam, clr: str) -> str | None: return "true" if value else "false" if isinstance(value, enum.Enum): return f"{clr}.{csharp_types._pascal(value.name)}" + if isinstance(value, str): + return json.dumps(value, ensure_ascii=True) raise GeneratorError(f"{p.name}: cannot render the default {value!r} in C#") @@ -132,14 +139,19 @@ def _param(m: RegistryMethod, p: RegistryParam, model: csharp_types._Model) -> C raise GeneratorError(f"{origin}: C# parameter {name} does not convert back to its wire name") nullable = "?" if p.field.nullable else "" token = f"{csharp_types._xml_text(str(p.field.type))}{', nullable' if p.field.nullable else ''}" - if p.record is not None: + if (m.name, p.name) in CSHARP_RAW_PARAMS: + require_binary(p.field, origin, "a canonical record's raw envelope") + clr = "byte[]" + doc = f"The packed {p.record} envelope, validated by the service ({token})." + attrs: tuple[str, ...] = () + elif p.record is not None: require_binary(p.field, origin, "a record's column") record = csharp_types.csharp_name(p.record) if record not in model.records: raise GeneratorError(f"{origin}: {record} is not a generated C# record") clr = record doc = f"The packed {p.record} ({token})." - attrs: tuple[str, ...] = () + attrs = () else: mapped = csharp_types._map(p.field.type, p.annotation, model=model, origin=origin) clr = mapped.clr.replace(_GENERIC_NS, "") @@ -180,7 +192,7 @@ def _method_name(wire: str) -> str: def csharp_methods(methods: Sequence[RegistryMethod] | None = None) -> list[CSharpMethod]: - """Every ``vgi.v2`` method as vgi-csharp declares it.""" + """Every ``vgi.v3`` method as vgi-csharp declares it.""" methods = registry_methods() if methods is None else methods model = csharp_types.build_model() check_raw_results(CSHARP_RAW_RESULTS, methods, what="CSHARP_RAW_RESULTS") @@ -229,7 +241,7 @@ def _render_method(cm: CSharpMethod) -> list[str]: _INTERFACE_DOC = """\ /// -/// The vgi.v2 protocol: every method a VGI worker serves, generated from the reference +/// The vgi.v3 protocol: every method a VGI worker serves, generated from the reference /// (vgi.protocol.VgiProtocol in vgi-python). /// /// @@ -237,7 +249,7 @@ def _render_method(cm: CSharpMethod) -> list[str]: /// below are the wire contract: a parameter's CLR type and nullability derive its /// params-schema column, the return type derives the result column, and /// [StreamHeader] names a stream's header. They reproduce the reference's schemas exactly, -/// which is what makes vgi_rpc.Reflection.v1 report one vgi.v2 hash in every SDK. The +/// which is what makes vgi_rpc.Reflection.v1 report one vgi.v3 hash in every SDK. The /// protocol is the unit of optionality: every method is registered, whether or not this worker /// implements it. /// Every method has a default body that answers UNIMPLEMENTED / @@ -362,7 +374,7 @@ def derive(self, text: str) -> list[DerivedMethod]: # noqa: D102 elif inner.startswith("RpcStream<"): kind = MethodKind.STREAM if hm := re.search(r"\[StreamHeader\(typeof\((\w+)\)\)\]", attrs): - expect(hm.group(1) in headers, f"{name}: [StreamHeader] {hm.group(1)} is no vgi.v2 header") + expect(hm.group(1) in headers, f"{name}: [StreamHeader] {hm.group(1)} is no vgi.v3 header") header = headers[hm.group(1)] else: kind = MethodKind.UNARY diff --git a/vgi/codegen/go_registry.py b/vgi/codegen/go_registry.py index f65bdda9..24008d4a 100644 --- a/vgi/codegen/go_registry.py +++ b/vgi/codegen/go_registry.py @@ -1,6 +1,6 @@ # Copyright 2025, 2026 Query Farm LLC - https://query.farm -r"""Emit vgi-go's ``vgi.v2`` registry: the service interface, its UNIMPLEMENTED base and the method table. +r"""Emit vgi-go's ``vgi.v3`` registry: the service interface, its UNIMPLEMENTED base and the method table. vgi-rpc-go registers a method explicitly -- ``vgirpc.Unary[P, R](s, name, handler)`` -- and derives its schemas by reflection over the Go types: the @@ -18,7 +18,7 @@ vgi-rpc-go unwraps the request into); the generated file gives that struct its ``VgiRpcParamsSchema()``, the way ``stringer`` adds ``String()`` to a hand-written type, so the advertised ``request: binary`` comes from here. -- **``vgiService``**, every ``vgi.v2`` method with its exact params and result +- **``vgiService``**, every ``vgi.v3`` method with its exact params and result types, and **``unimplementedVgiService``**, whose every method answers ``methodNotImplemented`` -- ``UNIMPLEMENTED`` / ``method_not_implemented`` / ``" is not implemented by this worker"``. vgi-go's concrete service @@ -120,7 +120,7 @@ @dataclass(frozen=True) class GoMethod: - """One ``vgi.v2`` method as vgi-go declares and registers it.""" + """One ``vgi.v3`` method as vgi-go declares and registers it.""" method: RegistryMethod #: The Go method name (``CatalogSchemas``). @@ -164,7 +164,9 @@ def _params(m: RegistryMethod, pascal: str, request_field: list[pa.Field[Any]]) request_field[:] = [field] go = _record_type(record, m.name) if "." in go: - raise GeneratorError(f"{m.name}: cannot declare VgiRpcParamsSchema on {go} from another package") + # A local defined type retains the generated record's fields while + # allowing the RPC envelope method to live in this package. + go = go_types.go_name(record) + "Wire" return go, None name = f"{pascal}Params" @@ -206,7 +208,7 @@ def header_var(m: RegistryMethod) -> str: def go_methods(methods: Sequence[RegistryMethod] | None = None) -> tuple[list[GoMethod], pa.Field[Any]]: - """Every ``vgi.v2`` method as vgi-go declares it, and the shared ``request`` column.""" + """Every ``vgi.v3`` method as vgi-go declares it, and the shared ``request`` column.""" methods = registry_methods() if methods is None else methods _check_tables(methods) names = identifiers((m.name for m in methods), go_types.go_field_name, what="Go method") @@ -220,7 +222,7 @@ def go_methods(methods: Sequence[RegistryMethod] | None = None) -> tuple[list[Go hv = header_var(m) if m.header_schema is not None else None out.append(GoMethod(m, pascal, params_type, struct, _result(m), hv)) if not request_field: - raise GeneratorError("no vgi.v2 method carries a packed request") + raise GeneratorError("no vgi.v3 method carries a packed request") return out, request_field[0] @@ -273,7 +275,7 @@ def _schema_var(name: str, fields: list[pa.Field[Any]], origin: str) -> list[str def _struct(r: go_types.GoRecord) -> list[str]: lines = _comment( [ - f"{r.name} is the params of vgi.v2 {r.py_name}: one field per params-schema column, in order. " + f"{r.name} is the params of vgi.v3 {r.py_name}: one field per params-schema column, in order. " "vgi-rpc-go derives the advertised schema from these tags." ] ) @@ -308,9 +310,9 @@ def _registration(gm: GoMethod) -> str: _SERVICE_DOC = [ - "vgiService is the vgi.v2 protocol: every method a VGI worker serves, generated from the " + "vgiService is the vgi.v3 protocol: every method a VGI worker serves, generated from the " "reference (vgi.protocol.VgiProtocol in vgi-python).", - "registerVgiService hosts every method, so vgi_rpc.Reflection.v1 reports the same vgi.v2 hash " + "registerVgiService hosts every method, so vgi_rpc.Reflection.v1 reports the same vgi.v3 hash " "from every SDK: the params and result types below derive the reference's schemas exactly. The " "protocol is the unit of optionality -- every method is registered whether or not this worker " "implements it. An implementation embeds unimplementedVgiService and defines the methods it " @@ -357,7 +359,7 @@ def _render_body(model: Sequence[RegistryMethod]) -> str: if gm.header_var is not None and header is not None: lines.append("") record = gm.method.header_type.__name__ # type: ignore[union-attr] - lines += _comment([f"{gm.header_var} is the header of the vgi.v2 {gm.method.name} stream ({record})."]) + lines += _comment([f"{gm.header_var} is the header of the vgi.v3 {gm.method.name} stream ({record})."]) lines += _schema_var(gm.header_var, list(header), gm.method.name) # VgiRpcParamsSchema on each wrapped record struct, once per Go type. @@ -367,9 +369,17 @@ def _render_body(model: Sequence[RegistryMethod]) -> str: wrapped.setdefault(gm.params_type, []).append(gm.method.name) for go_type, names in sorted(wrapped.items()): lines.append("") + method = next(gm.method for gm in methods if gm.method.name == names[0]) + record = method.params[0].record + assert record is not None + generated = _record_type(record, method.name) + if generated.startswith("generated."): + lines += _comment([f"{go_type} preserves the generated record inside the packed request envelope."]) + lines.append(f"type {go_type} {generated}") + lines.append("") lines += _comment( [ - f"VgiRpcParamsSchema advertises the wrapped protocol shape of vgi.v2 {', '.join(names)}: " + f"VgiRpcParamsSchema advertises the wrapped protocol shape of vgi.v3 {', '.join(names)}: " f"one `request` column; vgi-rpc-go unwraps it into {go_type}'s fields." ] ) @@ -389,14 +399,14 @@ def _render_body(model: Sequence[RegistryMethod]) -> str: if i: lines.append("") kind = "stream" if gm.method.is_stream else "unary" - lines += _comment([f"{gm.name} is vgi.v2 {gm.method.name} ({kind}). {_doc(gm.method)}"], "\t") + lines += _comment([f"{gm.name} is vgi.v3 {gm.method.name} ({kind}). {_doc(gm.method)}"], "\t") lines.append(f"\t{_signature(gm, named=True)}") lines.append("}") lines.append("") lines += _comment( [ - "unimplementedVgiService answers every vgi.v2 method with UNIMPLEMENTED / " + "unimplementedVgiService answers every vgi.v3 method with UNIMPLEMENTED / " 'method_not_implemented (" is not implemented by this worker"). Embed it in a ' "vgiService and define the methods the worker serves." ] @@ -405,7 +415,7 @@ def _render_body(model: Sequence[RegistryMethod]) -> str: for gm in methods: m = gm.method lines.append("") - lines += _comment([f"{gm.name} answers UNIMPLEMENTED: vgi.v2 {m.name} is not implemented by this worker."]) + lines += _comment([f"{gm.name} answers UNIMPLEMENTED: vgi.v3 {m.name} is not implemented by this worker."]) lines.append(f"func (unimplementedVgiService) {_signature(gm, named=False)} {{") if m.is_stream: lines.append(f'\treturn nil, {NOT_IMPLEMENTED}("{m.name}")') @@ -418,7 +428,7 @@ def _render_body(model: Sequence[RegistryMethod]) -> str: lines.append("") lines += _comment( [ - "registerVgiService hosts every vgi.v2 method on s, each dispatched to svc. The table is " + "registerVgiService hosts every vgi.v3 method on s, each dispatched to svc. The table is " "the protocol's whole method set: a method svc does not implement still registers, and " "answers UNIMPLEMENTED." ] diff --git a/vgi/codegen/go_schemas.py b/vgi/codegen/go_schemas.py index 4391de95..7f9b307f 100644 --- a/vgi/codegen/go_schemas.py +++ b/vgi/codegen/go_schemas.py @@ -136,7 +136,7 @@ def _emit_type(dtype: pa.DataType, *, origin: str) -> str: child_exprs = [ _emit_field_literal(dtype.field(i), origin=f"{origin}[struct child {i}]") for i in range(dtype.num_fields) ] - return "arrow.StructOf(\n" + ",\n".join("\t\t" + c for c in child_exprs) + ",\n\t)" + return "arrow.StructOf(\n" + ",\n".join("\t\t" + c.replace("\n", "\n\t") for c in child_exprs) + ",\n\t)" if pa.types.is_timestamp(dtype): unit_map = { diff --git a/vgi/codegen/go_types.py b/vgi/codegen/go_types.py index 89c9fa3b..608948da 100644 --- a/vgi/codegen/go_types.py +++ b/vgi/codegen/go_types.py @@ -45,7 +45,7 @@ import pyarrow as pa from vgi.catalog.catalog_interface import CatalogAttachResult -from vgi.codegen._common import GeneratorError, provenance_comment +from vgi.codegen._common import SHARED_CONTRACT_TYPES, GeneratorError, provenance_comment from vgi.codegen.csharp_types import _base, _hints, _parse_docstring from vgi.protocol import CatalogContentsResponse, SchemaContents @@ -64,6 +64,7 @@ #: derives as ``list>``); it is checked on its own against #: ``generated.SchemaContentsSchema``. GO_TYPE_RECORDS: tuple[type, ...] = ( + *SHARED_CONTRACT_TYPES, CatalogAttachResult, CatalogContentsResponse, SchemaContents, diff --git a/vgi/codegen/java_registry.py b/vgi/codegen/java_registry.py index 5a0fe34e..2b28c4c2 100644 --- a/vgi/codegen/java_registry.py +++ b/vgi/codegen/java_registry.py @@ -1,6 +1,6 @@ # Copyright 2025, 2026 Query Farm LLC - https://query.farm -r"""Emit vgi-java's ``VgiService``: the whole ``vgi.v2`` registry, for vgi-java. +r"""Emit vgi-java's ``VgiService``: the whole ``vgi.v3`` registry, for vgi-java. vgi-rpc-java registers a protocol by reflecting over a Java interface: every public method is a wire method, its parameters derive the params schema @@ -11,7 +11,7 @@ :class:`vgi.protocol.VgiProtocol` (see :mod:`vgi.codegen._registry` for the model and the reasons): -- every ``vgi.v2`` method, with types chosen so the derivation reproduces the +- every ``vgi.v3`` method, with types chosen so the derivation reproduces the reference schemas exactly; - a ``default`` body on every method that throws vgi-rpc-java's ``MethodNotImplementedError`` -- ``UNIMPLEMENTED`` / @@ -193,7 +193,7 @@ def _result(m: RegistryMethod) -> tuple[str, tuple[str, ...]]: def java_methods(methods: Sequence[RegistryMethod] | None = None) -> list[JavaMethod]: - """Every ``vgi.v2`` method as vgi-java declares it.""" + """Every ``vgi.v3`` method as vgi-java declares it.""" methods = registry_methods() if methods is None else methods check_record_table(JAVA_TYPES, methods, what="JAVA_TYPES", headers=True) check_raw_results(JAVA_RAW_RESULTS, methods, what="JAVA_RAW_RESULTS") @@ -257,7 +257,7 @@ def _render_method(jm: JavaMethod) -> list[str]: _CLASS_DOC = """\ /** - * The {@code vgi.v2} protocol: every method a VGI worker serves, generated from + * The {@code vgi.v3} protocol: every method a VGI worker serves, generated from * the reference ({@code vgi.protocol.VgiProtocol} in vgi-python). * *

vgi-rpc-java reflects over this interface to register the protocol, so the @@ -265,7 +265,7 @@ def _render_method(jm: JavaMethod) -> list[str]: * {@code @Nullable} derive its params-schema column, the return type derives the * {@code result} column, and {@code @StreamHeader} names a stream's header. They * reproduce the reference's schemas exactly, which is what makes - * {@code vgi_rpc.Reflection.v1} report one {@code vgi.v2} hash in every SDK. The + * {@code vgi_rpc.Reflection.v1} report one {@code vgi.v3} hash in every SDK. The * protocol is the unit of optionality: every method is registered, whether or not * this worker implements it.

* @@ -435,7 +435,7 @@ def derive(self, text: str) -> list[DerivedMethod]: # noqa: D102 if ret.startswith("RpcStream<"): kind = MethodKind.STREAM if hm := re.search(r"@StreamHeader\((\w+)\.class\)", method_ann): - expect(hm.group(1) in headers, f"{name}: @StreamHeader({hm.group(1)}) is no vgi.v2 header") + expect(hm.group(1) in headers, f"{name}: @StreamHeader({hm.group(1)}) is no vgi.v3 header") header = headers[hm.group(1)] result = None elif ret == "void": diff --git a/vgi/codegen/java_types.py b/vgi/codegen/java_types.py index f3d30b82..4b1a9fe6 100644 --- a/vgi/codegen/java_types.py +++ b/vgi/codegen/java_types.py @@ -44,8 +44,8 @@ import pyarrow as pa from vgi.catalog.catalog_interface import CatalogAttachResult -from vgi.codegen._common import GeneratorError, provenance_comment -from vgi.protocol import CatalogContentsResponse, SchemaContents +from vgi.codegen._common import SHARED_CONTRACT_TYPES, GeneratorError, provenance_comment +from vgi.protocol import CatalogContentsResponse, MacroCreateRequest, SchemaContents if TYPE_CHECKING: from typing import TextIO @@ -60,9 +60,11 @@ #: Protocol classes emitted as Java records, one file each. JAVA_RECORDS: tuple[type, ...] = ( + *SHARED_CONTRACT_TYPES, CatalogAttachResult, SchemaContents, CatalogContentsResponse, + MacroCreateRequest, ) #: The Java name of a protocol class, where it is not the Python one. @@ -133,6 +135,11 @@ def _element(dtype: pa.DataType, origin: str) -> str: for proto, java in _ELEMENTS: if dtype.equals(proto): return java + if pa.types.is_list(dtype): + item = dtype.value_field + if item.name != "item" or not item.nullable: + raise GeneratorError(f"{origin}: vgi-rpc-java derives every list item as a nullable 'item'; got {item}.") + return f"List<{_element(item.type, origin + '[]')}>" if pa.types.is_struct(dtype): # A nested record derives as an inline struct of its components, so a # struct element is the generated record whose own schema is this struct. @@ -162,6 +169,9 @@ def _component(f: pa.Field[Any], origin: str) -> _Component: ann = (f"@ArrowField(ArrowFieldType.{override})",) if override else () return _Component(f.name, boxed if f.nullable else plain, (*nullable, *ann)) + if pa.types.is_struct(dtype): + return _Component(f.name, _element(dtype, where), nullable) + if pa.types.is_list(dtype): item = dtype.value_field if item.name != "item" or not item.nullable: diff --git a/vgi/codegen/protocol_version.py b/vgi/codegen/protocol_version.py index 7dcf03b6..83c0f9a5 100644 --- a/vgi/codegen/protocol_version.py +++ b/vgi/codegen/protocol_version.py @@ -9,11 +9,9 @@ Workflow: -1. Bump ``VgiProtocol.protocol_version`` in ``vgi/protocol.py``. -2. ``uv run vgi-gen-protocol-version > vgi/protocol_version.txt``. -3. Also regenerate ``vgi_protocol_constants.hpp`` in the C++ tree (see - ``vgi.codegen.cpp_constants``). -4. Commit both regenerated files in the same change. +1. Set ``VgiProtocol.protocol_name`` and ``protocol_version`` in ``vgi/protocol.py``. +2. Run ``python scripts/regen_generated.py --require-all`` with all SDK roots configured. +3. Commit the regenerated version, names, registries and contract fixtures together. ``tests/test_generated_protocol_version.py`` enforces that the checked-in file matches what the generator would emit right now — CI catches a stale diff --git a/vgi/codegen/reporting_docs.py b/vgi/codegen/reporting_docs.py index 0aa6f935..b532adb5 100644 --- a/vgi/codegen/reporting_docs.py +++ b/vgi/codegen/reporting_docs.py @@ -114,7 +114,7 @@ def render(record: str = "index") -> str: f"| [{title}](reference/{name}.md) | `vgi/reporting/{name}.py` |" for name, title in _TITLES.items() ) parts.append( - "\nSee [wire behavior](wire-contracts.md) for lifecycle, errors, encoding rules and " + "\nSee [wire behavior](README.md#shared-conventions) for lifecycle, errors, encoding rules and " "worker policy. Regenerate these pages with " "`uv run python scripts/regen_generated.py --only vgi-python`.\n" ) @@ -125,7 +125,7 @@ def render(record: str = "index") -> str: parts = [ f"# {title}: Python reference\n", generated, - "[All protocols](../python-contracts.md) · [Wire behavior](../wire-contracts.md)\n", + "[All protocols](../python-contracts.md) · [Wire behavior](../README.md#shared-conventions)\n", f"Source: `vgi/reporting/{record}.py`. Edit the Python module and regenerate this page.\n", ] if not any(p.__module__ == module.__name__ for p in PROTOCOLS): @@ -143,7 +143,12 @@ def render(record: str = "index") -> str: "resource_ownership": "objects", "schedules_v2": "schedules", }.get(record, record) - parts.append(f"[Behavior specification](../{spec}.md) · [Shared records](common.md)\n") + # These newer contracts currently have API references but no separate + # behavior specification. Keep generated links limited to existing pages. + if spec in {"objects", "resource_identity"}: + parts.append("[Shared records](common.md)\n") + else: + parts.append(f"[Behavior specification](../{spec}.md) · [Shared records](common.md)\n") protocol = next(p for p in PROTOCOLS if p.__module__ == module.__name__) parts.append(f"Protocol `{protocol.protocol_name}`, version `{protocol.protocol_version}`.\n") parts.append("## Methods\n") diff --git a/vgi/codegen/rust_registry.py b/vgi/codegen/rust_registry.py index 231b3d69..bca0624c 100644 --- a/vgi/codegen/rust_registry.py +++ b/vgi/codegen/rust_registry.py @@ -1,6 +1,6 @@ # Copyright 2025, 2026 Query Farm LLC - https://query.farm -r"""Emit vgi-rust's ``vgi_service.rs``: the whole ``vgi.v2`` registry, for vgi-rust. +r"""Emit vgi-rust's ``vgi_service.rs``: the whole ``vgi.v3`` registry, for vgi-rust. vgi-rpc-rust registers a protocol explicitly: one ``MethodInfo`` per method, each carrying its params / result / header Arrow schemas and a handler closure @@ -8,7 +8,7 @@ file carries all three pieces of the registry (see :mod:`vgi.codegen._registry` for the model and the reasons): -- ``trait VgiService`` -- one method per ``vgi.v2`` method, each with a default +- ``trait VgiService`` -- one method per ``vgi.v3`` method, each with a default body answering vgi-rpc-rust's ``RpcError::method_not_implemented`` (``UNIMPLEMENTED`` / ``method_not_implemented`` / ``" is not implemented by this worker"``). vgi-rust's ``Dispatcher`` implements it and @@ -29,7 +29,7 @@ hand-maintained type table here: nothing in a Rust signature can change a schema, because the schemas are in the table, not derived from the signature. -The one Rust-only choice is the stream kind: every ``vgi.v2`` stream registers +The one Rust-only choice is the stream kind: every ``vgi.v3`` stream registers as ``MethodType::Dynamic`` (the handler's ``StreamResult`` picks producer or exchange per call). The protocol hash records only ``"stream"``, so this is wire-identical. @@ -67,7 +67,7 @@ #: The generated module, relative to the vgi-rust checkout. TARGET = "vgi/src/protocol/vgi_service.rs" -#: The ``MethodType`` every ``vgi.v2`` stream registers with (see module doc). +#: The ``MethodType`` every ``vgi.v3`` stream registers with (see module doc). RUST_STREAM_KIND = "MethodType::Dynamic" @@ -188,14 +188,14 @@ def _trait_method(m: RegistryMethod) -> list[str]: _TRAIT_DOC = """\ -/// The `vgi.v2` protocol: every method a VGI worker serves, generated from the +/// The `vgi.v3` protocol: every method a VGI worker serves, generated from the /// reference (`vgi.protocol.VgiProtocol` in vgi-python). /// /// Every method has a default body answering `UNIMPLEMENTED` / /// `method_not_implemented` with `" is not implemented by this worker"`; /// an implementation overrides the methods it serves. [`register`] hosts every /// method, implemented or not, with the reference's schemas: the protocol is the -/// unit of optionality, and `vgi_rpc.Reflection.v1` reports one `vgi.v2` hash in +/// unit of optionality, and `vgi_rpc.Reflection.v1` reports one `vgi.v3` hash in /// every SDK. /// /// Each method receives the raw request, whose batch matches the method's params @@ -274,7 +274,7 @@ def _render_body(methods: Sequence[RegistryMethod]) -> str: lines.append("use vgi_rpc::{CallContext, MethodInfo, Request, Result, RpcError, RpcServer};") lines.append("") lines += [ - "/// Every `vgi.v2` method's wire name, sorted.", + "/// Every `vgi.v3` method's wire name, sorted.", "pub const METHOD_NAMES: &[&str] = &[", *[f' "{m.name}",' for m in methods], "];", @@ -300,7 +300,7 @@ def _render_body(methods: Sequence[RegistryMethod]) -> str: " Arc::new(Schema::new(fields))", "}", "", - "/// Register every `vgi.v2` method on `srv`, each delegating to `svc`.", + "/// Register every `vgi.v3` method on `srv`, each delegating to `svc`.", "///", "/// Methods `svc` does not override answer `UNIMPLEMENTED`, but are still", "/// registered with their full schemas, so the protocol hash is the reference's.", diff --git a/vgi/codegen/rust_types.py b/vgi/codegen/rust_types.py index bad9d90c..215a8f31 100644 --- a/vgi/codegen/rust_types.py +++ b/vgi/codegen/rust_types.py @@ -42,7 +42,7 @@ from vgi_rpc.rpc._types import MethodType, rpc_methods # type: ignore[attr-defined] from vgi.catalog.catalog_interface import CatalogAttachResult -from vgi.codegen._common import GeneratorError, provenance_comment, sanitize_name +from vgi.codegen._common import SHARED_CONTRACT_TYPES, GeneratorError, provenance_comment, sanitize_name from vgi.codegen.csharp_types import _base, _parse_docstring from vgi.codegen.rust_request_builders import _rust_ident from vgi.codegen.rust_schemas import snake_case @@ -59,6 +59,7 @@ #: factory for — an ``EXTRA_RESPONSE_TYPES`` entry or a unary method result) and #: delete its hand-written counterpart in vgi-rust. RUST_TYPES: tuple[type, ...] = ( + *SHARED_CONTRACT_TYPES, CatalogAttachResult, SchemaContents, CatalogContentsResponse, diff --git a/vgi/codegen/ts_client.py b/vgi/codegen/ts_client.py index 875e99fc..0067c1a7 100644 --- a/vgi/codegen/ts_client.py +++ b/vgi/codegen/ts_client.py @@ -24,12 +24,9 @@ ### Opaque vs describable types -For fields typed as `Annotated[T, ArrowType(binary|large_binary)]`: -- If `T` is a pyarrow `Schema` / `RecordBatch`, or an opaque dataclass - (`Arguments`, `ArrowSerializableDataclass | None`), emit `Uint8Array` with - a JSDoc tag noting the semantic source. Serialization stays the caller's - responsibility (and matches the existing Python side). -- Otherwise walk the inner type and emit its semantic shape. +For fields typed as `Annotated[T, ArrowType(binary|large_binary)]`, emit +`Uint8Array`: the codec returns bytes and does not recursively decode nested +records. Keep describable nested interfaces available for explicit decoding. For the INFO_TYPES response envelopes (CatalogsResponse, TablesResponse, … FunctionsResponse), promote `items: list[bytes]` to a typed array of the @@ -53,6 +50,7 @@ import typing from typing import TYPE_CHECKING +import pyarrow as pa from vgi_rpc.rpc._types import rpc_methods from vgi.codegen._common import INFO_TYPES, GeneratorError, provenance_comment @@ -194,6 +192,10 @@ def _emit_type(t: typing.Any, ctx: _Ctx) -> str: inner = _strip_optional(t) if _is_optional(t) else t suffix = " | null" if _is_optional(t) else "" + if pa.types.is_binary(arrow_meta.arrow_type) or pa.types.is_large_binary(arrow_meta.arrow_type): + if isinstance(inner, type) and dataclasses.is_dataclass(inner) and _opaque_tag_for(inner) is None: + _register_dataclass(inner, ctx) + return f"Uint8Array{suffix}" if _opaque_tag_for(inner) is not None: return f"Uint8Array{suffix}" if typing.get_origin(inner) is list: @@ -323,6 +325,12 @@ def _inner_opaque_semantic(t: typing.Any) -> str | None: tag = _opaque_tag_for(inner) if tag is not None: return tag + if ( + (pa.types.is_binary(arrow_meta.arrow_type) or pa.types.is_large_binary(arrow_meta.arrow_type)) + and isinstance(inner, type) + and dataclasses.is_dataclass(inner) + ): + return f"@ipc-record {inner.__name__}" if typing.get_origin(inner) is list: (li,) = typing.get_args(inner) tag = _opaque_tag_for(li) diff --git a/vgi/codegen/ts_registry.py b/vgi/codegen/ts_registry.py index 26dc8dbe..e217bc72 100644 --- a/vgi/codegen/ts_registry.py +++ b/vgi/codegen/ts_registry.py @@ -1,6 +1,6 @@ # Copyright 2025, 2026 Query Farm LLC - https://query.farm -r"""Emit vgi-typescript's ``vgi-service.ts``: the whole ``vgi.v2`` registry, for vgi-typescript. +r"""Emit vgi-typescript's ``vgi-service.ts``: the whole ``vgi.v3`` registry, for vgi-typescript. ``@query-farm/vgi-rpc`` registers methods explicitly -- ``protocol.unary(name, {params, result, handler})`` and @@ -10,7 +10,7 @@ :class:`vgi.protocol.VgiProtocol` (see :mod:`vgi.codegen._registry` for the model and the reasons): -- ``VGI_V2_METHODS``: the registration table. Every ``vgi.v2`` method with its +- ``VGI_V3_METHODS``: the registration table. Every ``vgi.v3`` method with its wire name, its ``VgiService`` key, and its params / result / header schemas, written in the vgi-typescript Arrow facade's DSL by :mod:`vgi.codegen.ts_schemas`' own field renderer, so they are the reference @@ -124,7 +124,7 @@ def _field_ts_type(f: pa.Field[Any], origin: str) -> str: @dataclass(frozen=True) class TsMethod: - """One rendered ``vgi.v2`` method.""" + """One rendered ``vgi.v3`` method.""" method: RegistryMethod key: str @@ -141,7 +141,7 @@ def _result_type(m: RegistryMethod) -> str | None: def ts_methods(methods: Sequence[RegistryMethod] | None = None) -> list[TsMethod]: - """Every ``vgi.v2`` method as vgi-typescript declares it.""" + """Every ``vgi.v3`` method as vgi-typescript declares it.""" methods = registry_methods() if methods is None else methods keys = identifiers((m.name for m in methods), camel, what="VgiService key") return [TsMethod(m, keys[m.name], m.schema_names.params, _result_type(m)) for m in methods] @@ -307,12 +307,12 @@ def _render_default(tm: TsMethod) -> str: """ _SERVICE_DOC = [ - "The `vgi.v2` protocol: every method a VGI worker serves, generated from the", + "The `vgi.v3` protocol: every method a VGI worker serves, generated from the", "reference (`vgi.protocol.VgiProtocol` in vgi-python).", "", "The protocol is the unit of optionality: every method is registered, whether", "or not this worker implements it, so `vgi_rpc.Reflection.v1` reports one", - "`vgi.v2` hash in every SDK. Build an implementation by spreading handlers", + "`vgi.v3` hash in every SDK. Build an implementation by spreading handlers", "over {@link UNIMPLEMENTED_VGI_SERVICE}; register it with", "{@link registerVgiService}. Never change a signature here by hand:", "regenerate it, so the change comes from the protocol.", @@ -339,11 +339,11 @@ def _render_default(tm: TsMethod) -> str: } /** - * Register every `vgi.v2` method on *protocol*, with the reference's schemas - * from {@link VGI_V2_METHODS} and the handlers from *service*. + * Register every `vgi.v3` method on *protocol*, with the reference's schemas + * from {@link VGI_V3_METHODS} and the handlers from *service*. */ export function registerVgiService(protocol: Protocol, service: VgiService): Protocol { - for (const m of VGI_V2_METHODS) { + for (const m of VGI_V3_METHODS) { if (m.type === "unary") { const handler = service[m.key] as (params: object, ctx: CallContext) => ReturnType; protocol.unary(m.name, { @@ -442,7 +442,7 @@ def _render_body(model: Sequence[RegistryMethod]) -> str: lines.append("// The registration table") lines.append("// " + "-" * 76) lines.append("") - lines += _jsdoc(["One `vgi.v2` method: its wire name, its `VgiService` member, and its schemas."], "") + lines += _jsdoc(["One `vgi.v3` method: its wire name, its `VgiService` member, and its schemas."], "") lines += [ "export type VgiMethodSpec =", " | {", @@ -466,13 +466,13 @@ def _render_body(model: Sequence[RegistryMethod]) -> str: ] lines += _jsdoc( [ - "Every `vgi.v2` method, sorted by wire name, with the reference's", + "Every `vgi.v3` method, sorted by wire name, with the reference's", "params / result / header schemas. This table is what", "{@link registerVgiService} registers, so it is what reflection hashes.", ], "", ) - lines.append("export const VGI_V2_METHODS: readonly VgiMethodSpec[] = [") + lines.append("export const VGI_V3_METHODS: readonly VgiMethodSpec[] = [") lines += specs lines.append("];") diff --git a/vgi/codegen/ts_types.py b/vgi/codegen/ts_types.py index d0a14447..b36baad6 100644 --- a/vgi/codegen/ts_types.py +++ b/vgi/codegen/ts_types.py @@ -59,6 +59,7 @@ from vgi.codegen._common import ( EXTRA_RESPONSE_TYPES, REQUEST_TYPES, + SHARED_CONTRACT_TYPES, GeneratorError, collect_schemas, provenance_comment, @@ -74,6 +75,7 @@ #: The records this module emits builders and codecs for. Append to extend. TS_RECORD_TYPES: tuple[type, ...] = ( + *SHARED_CONTRACT_TYPES, CatalogAttachResult, SchemaContents, CatalogContentsResponse, @@ -298,7 +300,10 @@ def _render_record(r: _Record) -> str: lines.append(f"export function build{r.name}(init: {r.name}Init): {r.name} {{") lines.append(" return {") for f in r.fields: - if f.default is not None: + if f.default is not None and f.default != "null" and f.nullable: + # Explicit NULL is a wire value; only omission selects a default. + lines.append(f" {f.name}: init.{f.name} === undefined ? {f.default} : init.{f.name},") + elif f.default is not None: lines.append(f" {f.name}: init.{f.name} ?? {f.default},") elif f.nullable: lines.append(f" {f.name}: init.{f.name} ?? null,") diff --git a/vgi/column_path.py b/vgi/column_path.py new file mode 100644 index 00000000..57742505 --- /dev/null +++ b/vgi/column_path.py @@ -0,0 +1,244 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Raw, resolved column identities shared by catalog operations and filters. + +Components are canonical field names supplied by the catalog. Dots and quotes +are literal characters. SQL identifier resolution belongs to the engine adapter; +wire helpers must not split, unquote, or case-fold identifiers. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from dataclasses import dataclass, field +from typing import Any, TypeAlias + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass + +# vgi-rpc's runtime Arrow inference resolves TypeAlias but not TypeAliasType. +ColumnPath: TypeAlias = list[str] # noqa: UP040 +ColumnKey: TypeAlias = tuple[str, ...] # noqa: UP040 + + +def column_path_key(path: Sequence[str]) -> ColumnKey: + """Validate components and return an immutable, case-preserving identity.""" + if isinstance(path, (str, bytes)) or not isinstance(path, Sequence): + raise TypeError("column path must be a sequence of raw identifier components") + if not path: + raise ValueError("column path must contain at least one component") + if any(not isinstance(component, str) for component in path): + raise TypeError("column path components must be strings") + if any(not component or "\x00" in component for component in path): + raise ValueError("column path components must be nonempty and contain no NUL") + return tuple(path) + + +def resolve_column_path(schema: pa.Schema, path: Sequence[str]) -> pa.Field[Any]: + """Look up exact field names, rejecting missing, ambiguous or non-struct parents. + + A list element, map key, or union arm is not an implicit field traversal. + Supporting those operations requires an explicitly defined access operation. + """ + key = column_path_key(path) + fields: pa.Schema | pa.StructType = schema + for index, component in enumerate(key): + matches = [definition for definition in fields if definition.name == component] + if len(matches) != 1: + raise ValueError(f"column path {list(key)!r} is missing or ambiguous at {component!r}") + definition = matches[0] + if index < len(key) - 1: + if not pa.types.is_struct(definition.type): + raise ValueError(f"column path {list(key)!r} has a non-struct parent {component!r}") + fields = definition.type + return definition + + +@dataclass(frozen=True, kw_only=True) +class RequiredFilterPaths(ArrowSerializableDataclass): + """AND-of-OR groups of raw column paths required by a table scan. + + A group is satisfied when a present filter path is a component prefix of + at least one member. Filtering ``["a"]`` satisfies ``["a", "b"]``; + filtering ``["a.b"]`` does not. An empty outer list means no requirements; + an empty group is invalid. These are column-presence requirements, not a + proof that a predicate was enforced remotely or that it restricts any rows. + + Attributes: + groups: Ordered AND groups, each containing one or more OR alternatives. + """ + + groups: list[list[ColumnPath]] = field(default_factory=list) + + def __post_init__(self) -> None: + """Reject malformed paths and empty or duplicate alternatives.""" + self._keys() + + def _keys(self) -> list[list[ColumnKey]]: + if not isinstance(self.groups, list) or any(not isinstance(group, list) for group in self.groups): + raise TypeError("required filter groups must be lists of path alternatives") + keys = [] + for group in self.groups: + if not group: + raise ValueError("required filter groups must not be empty") + alternatives = [column_path_key(path) for path in group] + if len(set(alternatives)) != len(alternatives): + raise ValueError("required filter group contains duplicate paths") + keys.append(alternatives) + return keys + + def validate_schema(self, schema: pa.Schema) -> None: + """Require every alternative to identify a field in the table schema.""" + for group in self._keys(): + for path in group: + resolve_column_path(schema, path) + + def is_satisfied_by(self, present: Iterable[Sequence[str]]) -> bool: + """Check structural prefixes without conflating literal dotted names.""" + requirements = self._keys() + keys = {column_path_key(path) for path in present} + return all( + any(required[:length] in keys for required in group for length in range(1, len(required) + 1)) + for group in requirements + ) + + @classmethod + def from_legacy(cls, groups: list[list[str]], *, schema: pa.Schema) -> RequiredFilterPaths: + """Resolve legacy dotted strings only when the schema proves one identity. + + This is an explicit migration helper, never a wire-decoding fallback. + A legacy name matching both a literal dotted column and nested fields + is rejected so the catalog author can choose the intended components. + """ + paths: dict[str, list[ColumnPath]] = {} + + def collect(fields: pa.Schema | pa.StructType, prefix: ColumnPath) -> None: + for definition in fields: + path = [*prefix, definition.name] + column_path_key(path) + paths.setdefault(".".join(path), []).append(path) + if pa.types.is_struct(definition.type): + collect(definition.type, path) + + collect(schema, []) + if not isinstance(groups, list) or any(not isinstance(group, list) for group in groups): + raise TypeError("legacy required filter groups must be lists") + result = [] + for group in groups: + alternatives = [] + for member in group: + if not isinstance(member, str): + raise TypeError("legacy required filter members must be strings") + matches = paths.get(member, []) + if len(matches) != 1: + raise ValueError(f"legacy required filter {member!r} is missing or ambiguous; use raw components") + alternatives.append(matches[0]) + result.append(alternatives) + return cls(groups=result) + + +def single_column_name(path: Sequence[str]) -> str: + """Adapt one raw component to a flat provider; never join nested components.""" + key = column_path_key(path) + if len(key) != 1: + raise NotImplementedError("provider does not support nested column paths") + return key[0] + + +def column_definition(payload: bytes, path: Sequence[str], *, type_only: bool = False) -> pa.Field[Any]: + """Decode exactly one field whose name agrees with the explicit target path. + + Type changes use a plain nullable field as a type carrier. Nullability and + metadata must be changed through their own operations, never discarded. + """ + from vgi.catalog.column_metadata import validate_column_metadata + + key = column_path_key(path) + reader = pa.BufferReader(payload) + # The Arrow stubs omit NativeFile, which read_schema accepts at runtime. + schema = pa.ipc.read_schema(reader) # type: ignore[arg-type] + validate_column_metadata(schema) + if reader.tell() != len(payload) or len(schema) != 1 or schema.metadata: + raise ValueError("column definition must contain exactly one field and no schema metadata or trailing bytes") + result = schema.field(0) + if result.name != key[-1]: + raise ValueError("column definition name must match the last column path component") + attributes = { + b"default", + b"generated_expression", + b"comment", + b"tags", + b"compression", + b"vgi.column.default.v1", + b"vgi.column.generated.v1", + b"vgi.column.generation.v1", + } + if type_only: + if not result.nullable: + raise ValueError("type change field must contain only the target type and name") + pending = [result] + while pending: + field = pending.pop() + metadata = field.metadata or {} + if attributes.intersection(metadata) or any( + key.startswith((b"vgi.column.default.", b"vgi.column.generated.", b"vgi.column.generation.")) + for key in metadata + ): + raise ValueError("type change field must contain only the target type and name") + dtype = field.type + while isinstance(dtype, pa.BaseExtensionType) or pa.types.is_dictionary(dtype): + dtype = dtype.storage_type if isinstance(dtype, pa.BaseExtensionType) else dtype.value_type + pending.extend(dtype.field(index) for index in range(dtype.num_fields)) + return result + + +def replace_column_field( + schema: pa.Schema, + path: Sequence[str], + replacement: pa.Field[Any] | None, + *, + add: bool = False, +) -> pa.Schema: + """Build a new schema after a field edit, preserving unrelated metadata. + + Adding appends to the target's immediate parent. Replacing retains its + position, including a rename. Removing the last field of a table or struct + is rejected. The caller owns data rewrites, constraints and dependencies. + """ + key = column_path_key(path) + if replacement is not None: + column_path_key([replacement.name]) + if add and (replacement is None or replacement.name != key[-1]): + raise ValueError("added field must match the last column path component") + + def edit(fields: list[pa.Field[Any]], depth: int) -> list[pa.Field[Any]]: + matches = [i for i, definition in enumerate(fields) if definition.name == key[depth]] + if len(matches) > 1: + raise ValueError("column path is ambiguous") + if depth == len(key) - 1 and add: + if matches: + raise ValueError("column already exists") + assert replacement is not None + return [*fields, replacement] + if not matches: + raise ValueError("column path does not exist") + index = matches[0] + definition = fields[index] + if depth != len(key) - 1: + if not pa.types.is_struct(definition.type): + raise ValueError("column path has a non-struct parent") + updated = definition.with_type(pa.struct(edit(list(definition.type), depth + 1))) + elif replacement is None: + if len(fields) == 1: + raise ValueError("cannot drop the last column or struct field") + return fields[:index] + fields[index + 1 :] + else: + if any(i != index and field.name == replacement.name for i, field in enumerate(fields)): + raise ValueError("column already exists") + updated = replacement + return fields[:index] + [updated] + fields[index + 1 :] + + metadata: dict[bytes | str, bytes | str] | None = ( + {key: value for key, value in schema.metadata.items()} if schema.metadata else None + ) + return pa.schema(edit(list(schema), 0), metadata=metadata) diff --git a/vgi/exceptions.py b/vgi/exceptions.py index d142a649..565729e8 100644 --- a/vgi/exceptions.py +++ b/vgi/exceptions.py @@ -23,6 +23,7 @@ "ArgumentTypeError", "BindStateNotFoundError", "CatalogReadOnlyError", + "CatalogCapabilityError", "ExecutionIdentifierError", "InvalidArgumentError", "NotFoundError", @@ -70,6 +71,17 @@ class CatalogReadOnlyError(Exception): error_code: ClassVar[Code] = Code.FAILED_PRECONDITION +class CatalogCapabilityError(NotImplementedError): + """An operation requires a profile not selected or no longer supported.""" + + error_code: ClassVar[str] = "UNIMPLEMENTED" + error_kind: ClassVar[str] = "catalog_capability_required" + + def __init__(self, operation: str, profile: str) -> None: + """Identify the unsupported operation without exposing provider state.""" + super().__init__(f"{operation} requires negotiated capability {profile}") + + class OpaqueDataNotRecognizedError(ValueError): """An opaque value (``attach_opaque_data`` / ``transaction_opaque_data``) failed to open. diff --git a/vgi/http/vgi-client.js b/vgi/http/vgi-client.js index 734f7ddf..20375566 100644 --- a/vgi/http/vgi-client.js +++ b/vgi/http/vgi-client.js @@ -1,267 +1,13 @@ -var UD=Object.defineProperty;var MD=($)=>$;function LD($,Z){this[$]=MD.bind(null,Z)}var P8=($,Z)=>{for(var Q in Z)UD($,Q,{get:Z[Q],enumerable:!0,configurable:!0,set:LD.bind(Z,Q)})};var O=($,Z)=>()=>($&&(Z=$($=0)),Z);function O9($,Z){var Q={};for(var X in $)if(Object.prototype.hasOwnProperty.call($,X)&&Z.indexOf(X)<0)Q[X]=$[X];if($!=null&&typeof Object.getOwnPropertySymbols==="function"){for(var J=0,X=Object.getOwnPropertySymbols($);J=$.length)$=void 0;return{value:$&&$[X++],done:!$}}};throw TypeError(Z?"Object is not iterable.":"Symbol.iterator is not defined.")}function V0($){return this instanceof V0?(this.v=$,this):new V0($)}function UQ($,Z,Q){if(!Symbol.asyncIterator)throw TypeError("Symbol.asyncIterator is not defined.");var X=Q.apply($,Z||[]),J,W=[];return J=Object.create((typeof AsyncIterator==="function"?AsyncIterator:Object).prototype),G("next"),G("throw"),G("return",Y),J[Symbol.asyncIterator]=function(){return this},J;function Y(M){return function(L){return Promise.resolve(L).then(M,P)}}function G(M,L){if(X[M]){if(J[M]=function(D){return new Promise(function(R,V){W.push([M,D,R,V])>1||H(M,D)})},L)J[M]=L(J[M])}}function H(M,L){try{z(X[M](L))}catch(D){U(W[0][3],D)}}function z(M){M.value instanceof V0?Promise.resolve(M.value.v).then(q,P):U(W[0][2],M)}function q(M){H("next",M)}function P(M){H("throw",M)}function U(M,L){if(M(L),W.shift(),W.length)H(W[0][0],W[0][1])}}function d6($){var Z,Q;return Z={},X("next"),X("throw",function(J){throw J}),X("return"),Z[Symbol.iterator]=function(){return this},Z;function X(J,W){Z[J]=$[J]?function(Y){return(Q=!Q)?{value:V0($[J](Y)),done:!1}:W?W(Y):Y}:W}}function F8($){if(!Symbol.asyncIterator)throw TypeError("Symbol.asyncIterator is not defined.");var Z=$[Symbol.asyncIterator],Q;return Z?Z.call($):($=typeof D9==="function"?D9($):$[Symbol.iterator](),Q={},X("next"),X("throw"),X("return"),Q[Symbol.asyncIterator]=function(){return this},Q);function X(W){Q[W]=$[W]&&function(Y){return new Promise(function(G,H){Y=$[W](Y),J(G,H,Y.done,Y.value)})}}function J(W,Y,G,H){Promise.resolve(H).then(function(z){W({value:z,done:G})},Y)}}var MQ=()=>{};var C9,n6,AD,LQ=($)=>AD.encode($);var jX=O(()=>{C9=new TextDecoder("utf-8"),n6=C9.decode.bind(C9),AD=new TextEncoder});var DD=($)=>typeof $==="number",j9=($)=>typeof $==="boolean",o$=($)=>typeof $==="function",TZ=($)=>$!=null&&Object($)===$,N8=($)=>{return TZ($)&&o$($.then)},U8=($)=>{return TZ($)&&o$($[Symbol.iterator])},fQ=($)=>{return TZ($)&&o$($[Symbol.asyncIterator])},a1=($)=>{return TZ($)&&TZ($.schema)},t1=($)=>{return TZ($)&&"done"in $&&"value"in $},e1=($)=>{return TZ($)&&o$($.stat)&&DD($.fd)},$4=($)=>{return TZ($)&&PY($.body)},Z4=($)=>("_getDOMStream"in $)&&("_getNodeStream"in $),V9=($)=>{return TZ($)&&o$($.abort)&&o$($.getWriter)&&!Z4($)},PY=($)=>{return TZ($)&&o$($.cancel)&&o$($.getReader)&&!Z4($)},R9=($)=>{return TZ($)&&o$($.end)&&o$($.write)&&j9($.writable)&&!Z4($)},Q4=($)=>{return TZ($)&&o$($.read)&&o$($.pipe)&&j9($.readable)&&!Z4($)},E9=($)=>{return TZ($)&&o$($.clear)&&o$($.bytes)&&o$($.position)&&o$($.setPosition)&&o$($.capacity)&&o$($.getBufferIdentifier)&&o$($.createLong)};var qK={};P8(qK,{toUint8ClampedArrayIterator:()=>yD,toUint8ClampedArrayAsyncIterator:()=>lD,toUint8ClampedArray:()=>BD,toUint8ArrayIterator:()=>KK,toUint8ArrayAsyncIterator:()=>HK,toUint8Array:()=>$0,toUint32ArrayIterator:()=>gD,toUint32ArrayAsyncIterator:()=>uD,toUint32Array:()=>RD,toUint16ArrayIterator:()=>TD,toUint16ArrayAsyncIterator:()=>fD,toUint16Array:()=>VD,toInt8ArrayIterator:()=>kD,toInt8ArrayAsyncIterator:()=>hD,toInt8Array:()=>CD,toInt32ArrayIterator:()=>_D,toInt32ArrayAsyncIterator:()=>mD,toInt32Array:()=>oJ,toInt16ArrayIterator:()=>ID,toInt16ArrayAsyncIterator:()=>bD,toInt16Array:()=>jD,toFloat64ArrayIterator:()=>vD,toFloat64ArrayAsyncIterator:()=>cD,toFloat64Array:()=>SD,toFloat32ArrayIterator:()=>xD,toFloat32ArrayAsyncIterator:()=>pD,toFloat32Array:()=>wD,toBigUint64Array:()=>ED,toBigInt64Array:()=>X4,toArrayBufferViewIterator:()=>d8,toArrayBufferViewAsyncIterator:()=>M8,toArrayBufferView:()=>S0,rebaseValueOffsets:()=>J4,memcpy:()=>FY,joinUint8Arrays:()=>AQ,compareArrayLike:()=>zK});function OD($){let Z=$[0]?[$[0]]:[],Q,X,J,W;for(let Y,G,H=0,z=0,q=$.length;++Hq+P.byteLength,0),J,W,Y,G=0,H=-1,z=Math.min(Z||Number.POSITIVE_INFINITY,X);for(let q=Q.length;++H0)do if($[Q]!==Z[Q])return!1;while(++QS0(Int8Array,$),jD=($)=>S0(Int16Array,$),oJ=($)=>S0(Int32Array,$),X4=($)=>S0(BigInt64Array,$),$0=($)=>S0(Uint8Array,$),VD=($)=>S0(Uint16Array,$),RD=($)=>S0(Uint32Array,$),ED=($)=>S0(BigUint64Array,$),wD=($)=>S0(Float32Array,$),SD=($)=>S0(Float64Array,$),BD=($)=>S0(Uint8ClampedArray,$),YK=($)=>{return $.next(),$},kD=($)=>d8(Int8Array,$),ID=($)=>d8(Int16Array,$),_D=($)=>d8(Int32Array,$),KK=($)=>d8(Uint8Array,$),TD=($)=>d8(Uint16Array,$),gD=($)=>d8(Uint32Array,$),xD=($)=>d8(Float32Array,$),vD=($)=>d8(Float64Array,$),yD=($)=>d8(Uint8ClampedArray,$),hD=($)=>M8(Int8Array,$),bD=($)=>M8(Int16Array,$),mD=($)=>M8(Int32Array,$),HK=($)=>M8(Uint8Array,$),fD=($)=>M8(Uint16Array,$),uD=($)=>M8(Uint32Array,$),pD=($)=>M8(Float32Array,$),cD=($)=>M8(Float64Array,$),lD=($)=>M8(Uint8ClampedArray,$);var s$=O(()=>{MQ();jX();GK=typeof SharedArrayBuffer<"u"?SharedArrayBuffer:ArrayBuffer});function*dD($){let Z,Q=!1,X=[],J,W,Y,G=0;function H(){if(W==="peek")return AQ(X,Y)[0];return[J,X,G]=AQ(X,Y),J}({cmd:W,size:Y}=(yield(()=>null)())||{cmd:"read",size:0});let z=KK($)[Symbol.iterator]();try{do{if({done:Z,value:J}=Number.isNaN(Y-G)?z.next():z.next(Y-G),!Z&&J.byteLength>0)X.push(J),G+=J.byteLength;if(Z||Y<=G)do({cmd:W,size:Y}=yield H());while(Ynull)()))||{cmd:"read",size:0});let q=HK($)[Symbol.asyncIterator]();try{do{if({done:Q,value:W}=Number.isNaN(G-H)?yield V0(q.next()):yield V0(q.next(G-H)),!Q&&W.byteLength>0)J.push(W),H+=W.byteLength;if(Q||G<=H)do({cmd:Y,size:G}=yield yield V0(z()));while(Gnull)()))||{cmd:"read",size:0});let q=new w9($);try{do{if({done:Q,value:W}=Number.isNaN(G-H)?yield V0(q.read()):yield V0(q.read(G-H)),!Q&&W.byteLength>0)J.push($0(W)),H+=W.byteLength;if(Q||G<=H)do({cmd:Y,size:G}=yield yield V0(z()));while(G{})}get closed(){return this.reader?this.reader.closed.catch(()=>{}):Promise.resolve()}releaseLock(){if(this.reader)this.reader.releaseLock();this.reader=null}cancel($){return n(this,void 0,void 0,function*(){let{reader:Z,source:Q}=this;Z&&(yield Z.cancel($).catch(()=>{})),Q&&(Q.locked&&this.releaseLock())})}read($){return n(this,void 0,void 0,function*(){if($===0)return{done:this.reader==null,value:new Uint8Array(0)};let Z=yield this.reader.read();return!Z.done&&(Z.value=$0(Z)),Z})}}function sD($){return UQ(this,arguments,function*(){let Q=[],X="error",J=!1,W=null,Y,G,H=0,z=[],q;function P(){if(Y==="peek")return AQ(z,G)[0];return[q,z,H]=AQ(z,G),q}if({cmd:Y,size:G}=(yield yield V0((()=>null)()))||{cmd:"read",size:0},$.isTTY)return yield yield V0(new Uint8Array(0)),yield V0(null);try{Q[0]=PK($,"end"),Q[1]=PK($,"error");do{if(Q[2]=PK($,"readable"),[X,W]=yield V0(Promise.race(Q.map((M)=>M[2]))),X==="error")break;if(!(J=X==="end")){if(!Number.isFinite(G-H))q=$0($.read());else if(q=$0($.read(G-H)),q.byteLength0)z.push(q),H+=q.byteLength}if(J||G<=H)do({cmd:Y,size:G}=yield yield V0(P()));while(G{for(let[V,g]of M)$.off(V,g);try{let V=$.destroy;V&&V.call($,L),L=void 0}catch(V){L=V||L}finally{L!=null?R(L):D()}})}})}var NZ,W4=($)=>{return $.next(),$},PK=($,Z)=>{let Q=(J)=>X([Z,J]),X;return[Z,Q,new Promise((J)=>(X=J)&&$.once(Z,Q))]};var NY=O(()=>{MQ();s$();NZ={fromIterable($){return W4(dD($))},fromAsyncIterable($){return W4(nD($))},fromDOMStream($){return W4(oD($))},fromNodeStream($){return W4(sD($))},toDOMStream($,Z){throw Error('"toDOMStream" not available in this environment')},toNodeStream($,Z){throw Error('"toNodeStream" not available in this environment')}}});var i0;var Y4=O(()=>{(function($){$[$.V1=0]="V1",$[$.V2=1]="V2",$[$.V3=2]="V3",$[$.V4=3]="V4",$[$.V5=4]="V5"})(i0||(i0={}))});var m0;var FK=O(()=>{(function($){$[$.Sparse=0]="Sparse",$[$.Dense=1]="Dense"})(m0||(m0={}))});var J$;var NK=O(()=>{(function($){$[$.HALF=0]="HALF",$[$.SINGLE=1]="SINGLE",$[$.DOUBLE=2]="DOUBLE"})(J$||(J$={}))});var r0;var UK=O(()=>{(function($){$[$.DAY=0]="DAY",$[$.MILLISECOND=1]="MILLISECOND"})(r0||(r0={}))});var l;var UY=O(()=>{(function($){$[$.SECOND=0]="SECOND",$[$.MILLISECOND=1]="MILLISECOND",$[$.MICROSECOND=2]="MICROSECOND",$[$.NANOSECOND=3]="NANOSECOND"})(l||(l={}))});var s0;var MK=O(()=>{(function($){$[$.YEAR_MONTH=0]="YEAR_MONTH",$[$.DAY_TIME=1]="DAY_TIME",$[$.MONTH_DAY_NANO=2]="MONTH_DAY_NANO"})(s0||(s0={}))});var MY=2,DQ=4,uQ=4,t=4;var n8,G4,K4,o6;var LK=O(()=>{n8=new Int32Array(2),G4=new Float32Array(n8.buffer),K4=new Float64Array(n8.buffer),o6=new Uint16Array(new Uint8Array([1,0]).buffer)[0]===1});var LY;var AK=O(()=>{(function($){$[$.UTF8_BYTES=1]="UTF8_BYTES",$[$.UTF16_STRING=2]="UTF16_STRING"})(LY||(LY={}))});class UZ{constructor($){this.bytes_=$,this.position_=0,this.text_decoder_=new TextDecoder}static allocate($){return new UZ(new Uint8Array($))}clear(){this.position_=0}bytes(){return this.bytes_}position(){return this.position_}setPosition($){this.position_=$}capacity(){return this.bytes_.length}readInt8($){return this.readUint8($)<<24>>24}readUint8($){return this.bytes_[$]}readInt16($){return this.readUint16($)<<16>>16}readUint16($){return this.bytes_[$]|this.bytes_[$+1]<<8}readInt32($){return this.bytes_[$]|this.bytes_[$+1]<<8|this.bytes_[$+2]<<16|this.bytes_[$+3]<<24}readUint32($){return this.readInt32($)>>>0}readInt64($){return BigInt.asIntN(64,BigInt(this.readUint32($))+(BigInt(this.readUint32($+4))<>8}writeUint16($,Z){this.bytes_[$]=Z,this.bytes_[$+1]=Z>>8}writeInt32($,Z){this.bytes_[$]=Z,this.bytes_[$+1]=Z>>8,this.bytes_[$+2]=Z>>16,this.bytes_[$+3]=Z>>24}writeUint32($,Z){this.bytes_[$]=Z,this.bytes_[$+1]=Z>>8,this.bytes_[$+2]=Z>>16,this.bytes_[$+3]=Z>>24}writeInt64($,Z){this.writeInt32($,Number(BigInt.asIntN(32,Z))),this.writeInt32($+4,Number(BigInt.asIntN(32,Z>>BigInt(32))))}writeUint64($,Z){this.writeUint32($,Number(BigInt.asUintN(32,Z))),this.writeUint32($+4,Number(BigInt.asUintN(32,Z>>BigInt(32))))}writeFloat32($,Z){G4[0]=Z,this.writeInt32($,n8[0])}writeFloat64($,Z){K4[0]=Z,this.writeInt32($,n8[o6?0:1]),this.writeInt32($+4,n8[o6?1:0])}getBufferIdentifier(){if(this.bytes_.length{AK();LK()});class sJ{constructor($){this.minalign=1,this.vtable=null,this.vtable_in_use=0,this.isNested=!1,this.object_start=0,this.vtables=[],this.vector_num_elems=0,this.force_defaults=!1,this.string_maps=null,this.text_encoder=new TextEncoder;let Z;if(!$)Z=1024;else Z=$;this.bb=UZ.allocate(Z),this.space=Z}clear(){this.bb.clear(),this.space=this.bb.capacity(),this.minalign=1,this.vtable=null,this.vtable_in_use=0,this.isNested=!1,this.object_start=0,this.vtables=[],this.vector_num_elems=0,this.force_defaults=!1,this.string_maps=null}forceDefaults($){this.force_defaults=$}dataBuffer(){return this.bb}asUint8Array(){return this.bb.bytes().subarray(this.bb.position(),this.bb.position()+this.offset())}prep($,Z){if($>this.minalign)this.minalign=$;let Q=~(this.bb.capacity()-this.space+Z)+1&$-1;while(this.space=0&&this.vtable[Z]==0;Z--);let Q=Z+1;for(;Z>=0;Z--)this.addInt16(this.vtable[Z]!=0?$-this.vtable[Z]:0);let X=2;this.addInt16($-this.object_start);let J=(Q+X)*MY;this.addInt16(J);let W=0,Y=this.space;$:for(Z=0;Z=0;W--)this.writeInt8(J.charCodeAt(W))}if(this.prep(this.minalign,DQ+X),this.addOffset($),X)this.addInt32(this.bb.capacity()-this.space);this.bb.setPosition(this.space)}finishSizePrefixed($,Z){this.finish($,Z,!0)}requiredField($,Z){let Q=this.bb.capacity()-$,X=Q-this.bb.readInt32(Q);if(!(Z{DK()});var y0=O(()=>{LK();S9();DK();AK()});var iJ;var OK=O(()=>{(function($){$[$.BUFFER=0]="BUFFER"})(iJ||(iJ={}))});var gZ;var AY=O(()=>{(function($){$[$.LZ4_FRAME=0]="LZ4_FRAME",$[$.ZSTD=1]="ZSTD"})(gZ||(gZ={}))});class sZ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsBodyCompression($,Z){return(Z||new sZ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsBodyCompression($,Z){return $.setPosition($.position()+t),(Z||new sZ).__init($.readInt32($.position())+$.position(),$)}codec(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt8(this.bb_pos+$):gZ.LZ4_FRAME}method(){let $=this.bb.__offset(this.bb_pos,6);return $?this.bb.readInt8(this.bb_pos+$):iJ.BUFFER}static startBodyCompression($){$.startObject(2)}static addCodec($,Z){$.addFieldInt8(0,Z,gZ.LZ4_FRAME)}static addMethod($,Z){$.addFieldInt8(1,Z,iJ.BUFFER)}static endBodyCompression($){return $.endObject()}static createBodyCompression($,Z,Q){return sZ.startBodyCompression($),sZ.addCodec($,Z),sZ.addMethod($,Q),sZ.endBodyCompression($)}}var CK=O(()=>{y0();OK();AY()});class DY{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}offset(){return this.bb.readInt64(this.bb_pos)}length(){return this.bb.readInt64(this.bb_pos+8)}static sizeOf(){return 16}static createBuffer($,Z,Q){return $.prep(8,16),$.writeInt64(BigInt(Q!==null&&Q!==void 0?Q:0)),$.writeInt64(BigInt(Z!==null&&Z!==void 0?Z:0)),$.offset()}}class OY{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}length(){return this.bb.readInt64(this.bb_pos)}nullCount(){return this.bb.readInt64(this.bb_pos+8)}static sizeOf(){return 16}static createFieldNode($,Z,Q){return $.prep(8,16),$.writeInt64(BigInt(Q!==null&&Q!==void 0?Q:0)),$.writeInt64(BigInt(Z!==null&&Z!==void 0?Z:0)),$.offset()}}class MZ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsRecordBatch($,Z){return(Z||new MZ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsRecordBatch($,Z){return $.setPosition($.position()+t),(Z||new MZ).__init($.readInt32($.position())+$.position(),$)}length(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt64(this.bb_pos+$):BigInt("0")}nodes($,Z){let Q=this.bb.__offset(this.bb_pos,6);return Q?(Z||new OY).__init(this.bb.__vector(this.bb_pos+Q)+$*16,this.bb):null}nodesLength(){let $=this.bb.__offset(this.bb_pos,6);return $?this.bb.__vector_len(this.bb_pos+$):0}buffers($,Z){let Q=this.bb.__offset(this.bb_pos,8);return Q?(Z||new DY).__init(this.bb.__vector(this.bb_pos+Q)+$*16,this.bb):null}buffersLength(){let $=this.bb.__offset(this.bb_pos,8);return $?this.bb.__vector_len(this.bb_pos+$):0}compression($){let Z=this.bb.__offset(this.bb_pos,10);return Z?($||new sZ).__init(this.bb.__indirect(this.bb_pos+Z),this.bb):null}variadicBufferCounts($){let Z=this.bb.__offset(this.bb_pos,12);return Z?this.bb.readInt64(this.bb.__vector(this.bb_pos+Z)+$*8):BigInt(0)}variadicBufferCountsLength(){let $=this.bb.__offset(this.bb_pos,12);return $?this.bb.__vector_len(this.bb_pos+$):0}static startRecordBatch($){$.startObject(5)}static addLength($,Z){$.addFieldInt64(0,Z,BigInt("0"))}static addNodes($,Z){$.addFieldOffset(1,Z,0)}static startNodesVector($,Z){$.startVector(16,Z,8)}static addBuffers($,Z){$.addFieldOffset(2,Z,0)}static startBuffersVector($,Z){$.startVector(16,Z,8)}static addCompression($,Z){$.addFieldOffset(3,Z,0)}static addVariadicBufferCounts($,Z){$.addFieldOffset(4,Z,0)}static createVariadicBufferCountsVector($,Z){$.startVector(8,Z.length,8);for(let Q=Z.length-1;Q>=0;Q--)$.addInt64(Z[Q]);return $.endVector()}static startVariadicBufferCountsVector($,Z){$.startVector(8,Z,8)}static endRecordBatch($){return $.endObject()}}var jK=O(()=>{y0();CK()});class o8{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsDictionaryBatch($,Z){return(Z||new o8).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsDictionaryBatch($,Z){return $.setPosition($.position()+t),(Z||new o8).__init($.readInt32($.position())+$.position(),$)}id(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt64(this.bb_pos+$):BigInt("0")}data($){let Z=this.bb.__offset(this.bb_pos,6);return Z?($||new MZ).__init(this.bb.__indirect(this.bb_pos+Z),this.bb):null}isDelta(){let $=this.bb.__offset(this.bb_pos,8);return $?!!this.bb.readInt8(this.bb_pos+$):!1}static startDictionaryBatch($){$.startObject(3)}static addId($,Z){$.addFieldInt64(0,Z,BigInt("0"))}static addData($,Z){$.addFieldOffset(1,Z,0)}static addIsDelta($,Z){$.addFieldInt8(2,+Z,0)}static endDictionaryBatch($){return $.endObject()}}var B9=O(()=>{y0();jK()});var tX;var VK=O(()=>{(function($){$[$.Little=0]="Little",$[$.Big=1]="Big"})(tX||(tX={}))});var CY;var k9=O(()=>{(function($){$[$.DenseArray=0]="DenseArray"})(CY||(CY={}))});class RZ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsInt($,Z){return(Z||new RZ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsInt($,Z){return $.setPosition($.position()+t),(Z||new RZ).__init($.readInt32($.position())+$.position(),$)}bitWidth(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt32(this.bb_pos+$):0}isSigned(){let $=this.bb.__offset(this.bb_pos,6);return $?!!this.bb.readInt8(this.bb_pos+$):!1}static startInt($){$.startObject(2)}static addBitWidth($,Z){$.addFieldInt32(0,Z,0)}static addIsSigned($,Z){$.addFieldInt8(1,+Z,0)}static endInt($){return $.endObject()}static createInt($,Z,Q){return RZ.startInt($),RZ.addBitWidth($,Z),RZ.addIsSigned($,Q),RZ.endInt($)}}var H4=O(()=>{y0()});class L8{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsDictionaryEncoding($,Z){return(Z||new L8).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsDictionaryEncoding($,Z){return $.setPosition($.position()+t),(Z||new L8).__init($.readInt32($.position())+$.position(),$)}id(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt64(this.bb_pos+$):BigInt("0")}indexType($){let Z=this.bb.__offset(this.bb_pos,6);return Z?($||new RZ).__init(this.bb.__indirect(this.bb_pos+Z),this.bb):null}isOrdered(){let $=this.bb.__offset(this.bb_pos,8);return $?!!this.bb.readInt8(this.bb_pos+$):!1}dictionaryKind(){let $=this.bb.__offset(this.bb_pos,10);return $?this.bb.readInt16(this.bb_pos+$):CY.DenseArray}static startDictionaryEncoding($){$.startObject(4)}static addId($,Z){$.addFieldInt64(0,Z,BigInt("0"))}static addIndexType($,Z){$.addFieldOffset(1,Z,0)}static addIsOrdered($,Z){$.addFieldInt8(2,+Z,0)}static addDictionaryKind($,Z){$.addFieldInt16(3,Z,CY.DenseArray)}static endDictionaryEncoding($){return $.endObject()}}var RK=O(()=>{y0();k9();H4()});class K${constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsKeyValue($,Z){return(Z||new K$).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsKeyValue($,Z){return $.setPosition($.position()+t),(Z||new K$).__init($.readInt32($.position())+$.position(),$)}key($){let Z=this.bb.__offset(this.bb_pos,4);return Z?this.bb.__string(this.bb_pos+Z,$):null}value($){let Z=this.bb.__offset(this.bb_pos,6);return Z?this.bb.__string(this.bb_pos+Z,$):null}static startKeyValue($){$.startObject(2)}static addKey($,Z){$.addFieldOffset(0,Z,0)}static addValue($,Z){$.addFieldOffset(1,Z,0)}static endKeyValue($){return $.endObject()}static createKeyValue($,Z,Q){return K$.startKeyValue($),K$.addKey($,Z),K$.addValue($,Q),K$.endKeyValue($)}}var s6=O(()=>{y0()});class eX{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsBinary($,Z){return(Z||new eX).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsBinary($,Z){return $.setPosition($.position()+t),(Z||new eX).__init($.readInt32($.position())+$.position(),$)}static startBinary($){$.startObject(0)}static endBinary($){return $.endObject()}static createBinary($){return eX.startBinary($),eX.endBinary($)}}var I9=O(()=>{y0()});class $J{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsBinaryView($,Z){return(Z||new $J).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsBinaryView($,Z){return $.setPosition($.position()+t),(Z||new $J).__init($.readInt32($.position())+$.position(),$)}static startBinaryView($){$.startObject(0)}static endBinaryView($){return $.endObject()}static createBinaryView($){return $J.startBinaryView($),$J.endBinaryView($)}}var _9=O(()=>{y0()});class ZJ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsBool($,Z){return(Z||new ZJ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsBool($,Z){return $.setPosition($.position()+t),(Z||new ZJ).__init($.readInt32($.position())+$.position(),$)}static startBool($){$.startObject(0)}static endBool($){return $.endObject()}static createBool($){return ZJ.startBool($),ZJ.endBool($)}}var T9=O(()=>{y0()});class pQ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsDate($,Z){return(Z||new pQ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsDate($,Z){return $.setPosition($.position()+t),(Z||new pQ).__init($.readInt32($.position())+$.position(),$)}unit(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):r0.MILLISECOND}static startDate($){$.startObject(1)}static addUnit($,Z){$.addFieldInt16(0,Z,r0.MILLISECOND)}static endDate($){return $.endObject()}static createDate($,Z){return pQ.startDate($),pQ.addUnit($,Z),pQ.endDate($)}}var EK=O(()=>{y0();UK()});class EZ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsDecimal($,Z){return(Z||new EZ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsDecimal($,Z){return $.setPosition($.position()+t),(Z||new EZ).__init($.readInt32($.position())+$.position(),$)}precision(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt32(this.bb_pos+$):0}scale(){let $=this.bb.__offset(this.bb_pos,6);return $?this.bb.readInt32(this.bb_pos+$):0}bitWidth(){let $=this.bb.__offset(this.bb_pos,8);return $?this.bb.readInt32(this.bb_pos+$):128}static startDecimal($){$.startObject(3)}static addPrecision($,Z){$.addFieldInt32(0,Z,0)}static addScale($,Z){$.addFieldInt32(1,Z,0)}static addBitWidth($,Z){$.addFieldInt32(2,Z,128)}static endDecimal($){return $.endObject()}static createDecimal($,Z,Q,X){return EZ.startDecimal($),EZ.addPrecision($,Z),EZ.addScale($,Q),EZ.addBitWidth($,X),EZ.endDecimal($)}}var wK=O(()=>{y0()});class cQ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsDuration($,Z){return(Z||new cQ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsDuration($,Z){return $.setPosition($.position()+t),(Z||new cQ).__init($.readInt32($.position())+$.position(),$)}unit(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):l.MILLISECOND}static startDuration($){$.startObject(1)}static addUnit($,Z){$.addFieldInt16(0,Z,l.MILLISECOND)}static endDuration($){return $.endObject()}static createDuration($,Z){return cQ.startDuration($),cQ.addUnit($,Z),cQ.endDuration($)}}var SK=O(()=>{y0();UY()});class lQ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsFixedSizeBinary($,Z){return(Z||new lQ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsFixedSizeBinary($,Z){return $.setPosition($.position()+t),(Z||new lQ).__init($.readInt32($.position())+$.position(),$)}byteWidth(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt32(this.bb_pos+$):0}static startFixedSizeBinary($){$.startObject(1)}static addByteWidth($,Z){$.addFieldInt32(0,Z,0)}static endFixedSizeBinary($){return $.endObject()}static createFixedSizeBinary($,Z){return lQ.startFixedSizeBinary($),lQ.addByteWidth($,Z),lQ.endFixedSizeBinary($)}}var BK=O(()=>{y0()});class dQ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsFixedSizeList($,Z){return(Z||new dQ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsFixedSizeList($,Z){return $.setPosition($.position()+t),(Z||new dQ).__init($.readInt32($.position())+$.position(),$)}listSize(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt32(this.bb_pos+$):0}static startFixedSizeList($){$.startObject(1)}static addListSize($,Z){$.addFieldInt32(0,Z,0)}static endFixedSizeList($){return $.endObject()}static createFixedSizeList($,Z){return dQ.startFixedSizeList($),dQ.addListSize($,Z),dQ.endFixedSizeList($)}}var kK=O(()=>{y0()});class nQ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsFloatingPoint($,Z){return(Z||new nQ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsFloatingPoint($,Z){return $.setPosition($.position()+t),(Z||new nQ).__init($.readInt32($.position())+$.position(),$)}precision(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):J$.HALF}static startFloatingPoint($){$.startObject(1)}static addPrecision($,Z){$.addFieldInt16(0,Z,J$.HALF)}static endFloatingPoint($){return $.endObject()}static createFloatingPoint($,Z){return nQ.startFloatingPoint($),nQ.addPrecision($,Z),nQ.endFloatingPoint($)}}var IK=O(()=>{y0();NK()});class oQ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsInterval($,Z){return(Z||new oQ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsInterval($,Z){return $.setPosition($.position()+t),(Z||new oQ).__init($.readInt32($.position())+$.position(),$)}unit(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):s0.YEAR_MONTH}static startInterval($){$.startObject(1)}static addUnit($,Z){$.addFieldInt16(0,Z,s0.YEAR_MONTH)}static endInterval($){return $.endObject()}static createInterval($,Z){return oQ.startInterval($),oQ.addUnit($,Z),oQ.endInterval($)}}var _K=O(()=>{y0();MK()});class QJ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsLargeBinary($,Z){return(Z||new QJ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsLargeBinary($,Z){return $.setPosition($.position()+t),(Z||new QJ).__init($.readInt32($.position())+$.position(),$)}static startLargeBinary($){$.startObject(0)}static endLargeBinary($){return $.endObject()}static createLargeBinary($){return QJ.startLargeBinary($),QJ.endLargeBinary($)}}var g9=O(()=>{y0()});class XJ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsLargeUtf8($,Z){return(Z||new XJ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsLargeUtf8($,Z){return $.setPosition($.position()+t),(Z||new XJ).__init($.readInt32($.position())+$.position(),$)}static startLargeUtf8($){$.startObject(0)}static endLargeUtf8($){return $.endObject()}static createLargeUtf8($){return XJ.startLargeUtf8($),XJ.endLargeUtf8($)}}var x9=O(()=>{y0()});class JJ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsList($,Z){return(Z||new JJ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsList($,Z){return $.setPosition($.position()+t),(Z||new JJ).__init($.readInt32($.position())+$.position(),$)}static startList($){$.startObject(0)}static endList($){return $.endObject()}static createList($){return JJ.startList($),JJ.endList($)}}var v9=O(()=>{y0()});class sQ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsMap($,Z){return(Z||new sQ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsMap($,Z){return $.setPosition($.position()+t),(Z||new sQ).__init($.readInt32($.position())+$.position(),$)}keysSorted(){let $=this.bb.__offset(this.bb_pos,4);return $?!!this.bb.readInt8(this.bb_pos+$):!1}static startMap($){$.startObject(1)}static addKeysSorted($,Z){$.addFieldInt8(0,+Z,0)}static endMap($){return $.endObject()}static createMap($,Z){return sQ.startMap($),sQ.addKeysSorted($,Z),sQ.endMap($)}}var TK=O(()=>{y0()});class WJ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsNull($,Z){return(Z||new WJ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsNull($,Z){return $.setPosition($.position()+t),(Z||new WJ).__init($.readInt32($.position())+$.position(),$)}static startNull($){$.startObject(0)}static endNull($){return $.endObject()}static createNull($){return WJ.startNull($),WJ.endNull($)}}var y9=O(()=>{y0()});class YJ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsStruct_($,Z){return(Z||new YJ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsStruct_($,Z){return $.setPosition($.position()+t),(Z||new YJ).__init($.readInt32($.position())+$.position(),$)}static startStruct_($){$.startObject(0)}static endStruct_($){return $.endObject()}static createStruct_($){return YJ.startStruct_($),YJ.endStruct_($)}}var h9=O(()=>{y0()});class iZ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsTime($,Z){return(Z||new iZ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsTime($,Z){return $.setPosition($.position()+t),(Z||new iZ).__init($.readInt32($.position())+$.position(),$)}unit(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):l.MILLISECOND}bitWidth(){let $=this.bb.__offset(this.bb_pos,6);return $?this.bb.readInt32(this.bb_pos+$):32}static startTime($){$.startObject(2)}static addUnit($,Z){$.addFieldInt16(0,Z,l.MILLISECOND)}static addBitWidth($,Z){$.addFieldInt32(1,Z,32)}static endTime($){return $.endObject()}static createTime($,Z,Q){return iZ.startTime($),iZ.addUnit($,Z),iZ.addBitWidth($,Q),iZ.endTime($)}}var gK=O(()=>{y0();UY()});class rZ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsTimestamp($,Z){return(Z||new rZ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsTimestamp($,Z){return $.setPosition($.position()+t),(Z||new rZ).__init($.readInt32($.position())+$.position(),$)}unit(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):l.SECOND}timezone($){let Z=this.bb.__offset(this.bb_pos,6);return Z?this.bb.__string(this.bb_pos+Z,$):null}static startTimestamp($){$.startObject(2)}static addUnit($,Z){$.addFieldInt16(0,Z,l.SECOND)}static addTimezone($,Z){$.addFieldOffset(1,Z,0)}static endTimestamp($){return $.endObject()}static createTimestamp($,Z,Q){return rZ.startTimestamp($),rZ.addUnit($,Z),rZ.addTimezone($,Q),rZ.endTimestamp($)}}var xK=O(()=>{y0();UY()});class wZ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsUnion($,Z){return(Z||new wZ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsUnion($,Z){return $.setPosition($.position()+t),(Z||new wZ).__init($.readInt32($.position())+$.position(),$)}mode(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):m0.Sparse}typeIds($){let Z=this.bb.__offset(this.bb_pos,6);return Z?this.bb.readInt32(this.bb.__vector(this.bb_pos+Z)+$*4):0}typeIdsLength(){let $=this.bb.__offset(this.bb_pos,6);return $?this.bb.__vector_len(this.bb_pos+$):0}typeIdsArray(){let $=this.bb.__offset(this.bb_pos,6);return $?new Int32Array(this.bb.bytes().buffer,this.bb.bytes().byteOffset+this.bb.__vector(this.bb_pos+$),this.bb.__vector_len(this.bb_pos+$)):null}static startUnion($){$.startObject(2)}static addMode($,Z){$.addFieldInt16(0,Z,m0.Sparse)}static addTypeIds($,Z){$.addFieldOffset(1,Z,0)}static createTypeIdsVector($,Z){$.startVector(4,Z.length,4);for(let Q=Z.length-1;Q>=0;Q--)$.addInt32(Z[Q]);return $.endVector()}static startTypeIdsVector($,Z){$.startVector(4,Z,4)}static endUnion($){return $.endObject()}static createUnion($,Z,Q){return wZ.startUnion($),wZ.addMode($,Z),wZ.addTypeIds($,Q),wZ.endUnion($)}}var vK=O(()=>{y0();FK()});class GJ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsUtf8($,Z){return(Z||new GJ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsUtf8($,Z){return $.setPosition($.position()+t),(Z||new GJ).__init($.readInt32($.position())+$.position(),$)}static startUtf8($){$.startObject(0)}static endUtf8($){return $.endObject()}static createUtf8($){return GJ.startUtf8($),GJ.endUtf8($)}}var b9=O(()=>{y0()});class KJ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsUtf8View($,Z){return(Z||new KJ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsUtf8View($,Z){return $.setPosition($.position()+t),(Z||new KJ).__init($.readInt32($.position())+$.position(),$)}static startUtf8View($){$.startObject(0)}static endUtf8View($){return $.endObject()}static createUtf8View($){return KJ.startUtf8View($),KJ.endUtf8View($)}}var m9=O(()=>{y0()});var e0;var yK=O(()=>{(function($){$[$.NONE=0]="NONE",$[$.Null=1]="Null",$[$.Int=2]="Int",$[$.FloatingPoint=3]="FloatingPoint",$[$.Binary=4]="Binary",$[$.Utf8=5]="Utf8",$[$.Bool=6]="Bool",$[$.Decimal=7]="Decimal",$[$.Date=8]="Date",$[$.Time=9]="Time",$[$.Timestamp=10]="Timestamp",$[$.Interval=11]="Interval",$[$.List=12]="List",$[$.Struct_=13]="Struct_",$[$.Union=14]="Union",$[$.FixedSizeBinary=15]="FixedSizeBinary",$[$.FixedSizeList=16]="FixedSizeList",$[$.Map=17]="Map",$[$.Duration=18]="Duration",$[$.LargeBinary=19]="LargeBinary",$[$.LargeUtf8=20]="LargeUtf8",$[$.LargeList=21]="LargeList",$[$.RunEndEncoded=22]="RunEndEncoded",$[$.BinaryView=23]="BinaryView",$[$.Utf8View=24]="Utf8View",$[$.ListView=25]="ListView",$[$.LargeListView=26]="LargeListView"})(e0||(e0={}))});class WZ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsField($,Z){return(Z||new WZ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsField($,Z){return $.setPosition($.position()+t),(Z||new WZ).__init($.readInt32($.position())+$.position(),$)}name($){let Z=this.bb.__offset(this.bb_pos,4);return Z?this.bb.__string(this.bb_pos+Z,$):null}nullable(){let $=this.bb.__offset(this.bb_pos,6);return $?!!this.bb.readInt8(this.bb_pos+$):!1}typeType(){let $=this.bb.__offset(this.bb_pos,8);return $?this.bb.readUint8(this.bb_pos+$):e0.NONE}type($){let Z=this.bb.__offset(this.bb_pos,10);return Z?this.bb.__union($,this.bb_pos+Z):null}dictionary($){let Z=this.bb.__offset(this.bb_pos,12);return Z?($||new L8).__init(this.bb.__indirect(this.bb_pos+Z),this.bb):null}children($,Z){let Q=this.bb.__offset(this.bb_pos,14);return Q?(Z||new WZ).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+Q)+$*4),this.bb):null}childrenLength(){let $=this.bb.__offset(this.bb_pos,14);return $?this.bb.__vector_len(this.bb_pos+$):0}customMetadata($,Z){let Q=this.bb.__offset(this.bb_pos,16);return Q?(Z||new K$).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+Q)+$*4),this.bb):null}customMetadataLength(){let $=this.bb.__offset(this.bb_pos,16);return $?this.bb.__vector_len(this.bb_pos+$):0}static startField($){$.startObject(7)}static addName($,Z){$.addFieldOffset(0,Z,0)}static addNullable($,Z){$.addFieldInt8(1,+Z,0)}static addTypeType($,Z){$.addFieldInt8(2,Z,e0.NONE)}static addType($,Z){$.addFieldOffset(3,Z,0)}static addDictionary($,Z){$.addFieldOffset(4,Z,0)}static addChildren($,Z){$.addFieldOffset(5,Z,0)}static createChildrenVector($,Z){$.startVector(4,Z.length,4);for(let Q=Z.length-1;Q>=0;Q--)$.addOffset(Z[Q]);return $.endVector()}static startChildrenVector($,Z){$.startVector(4,Z,4)}static addCustomMetadata($,Z){$.addFieldOffset(6,Z,0)}static createCustomMetadataVector($,Z){$.startVector(4,Z.length,4);for(let Q=Z.length-1;Q>=0;Q--)$.addOffset(Z[Q]);return $.endVector()}static startCustomMetadataVector($,Z){$.startVector(4,Z,4)}static endField($){return $.endObject()}}var hK=O(()=>{y0();RK();s6();yK()});class _${constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsSchema($,Z){return(Z||new _$).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsSchema($,Z){return $.setPosition($.position()+t),(Z||new _$).__init($.readInt32($.position())+$.position(),$)}endianness(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):tX.Little}fields($,Z){let Q=this.bb.__offset(this.bb_pos,6);return Q?(Z||new WZ).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+Q)+$*4),this.bb):null}fieldsLength(){let $=this.bb.__offset(this.bb_pos,6);return $?this.bb.__vector_len(this.bb_pos+$):0}customMetadata($,Z){let Q=this.bb.__offset(this.bb_pos,8);return Q?(Z||new K$).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+Q)+$*4),this.bb):null}customMetadataLength(){let $=this.bb.__offset(this.bb_pos,8);return $?this.bb.__vector_len(this.bb_pos+$):0}features($){let Z=this.bb.__offset(this.bb_pos,10);return Z?this.bb.readInt64(this.bb.__vector(this.bb_pos+Z)+$*8):BigInt(0)}featuresLength(){let $=this.bb.__offset(this.bb_pos,10);return $?this.bb.__vector_len(this.bb_pos+$):0}static startSchema($){$.startObject(4)}static addEndianness($,Z){$.addFieldInt16(0,Z,tX.Little)}static addFields($,Z){$.addFieldOffset(1,Z,0)}static createFieldsVector($,Z){$.startVector(4,Z.length,4);for(let Q=Z.length-1;Q>=0;Q--)$.addOffset(Z[Q]);return $.endVector()}static startFieldsVector($,Z){$.startVector(4,Z,4)}static addCustomMetadata($,Z){$.addFieldOffset(2,Z,0)}static createCustomMetadataVector($,Z){$.startVector(4,Z.length,4);for(let Q=Z.length-1;Q>=0;Q--)$.addOffset(Z[Q]);return $.endVector()}static startCustomMetadataVector($,Z){$.startVector(4,Z,4)}static addFeatures($,Z){$.addFieldOffset(3,Z,0)}static createFeaturesVector($,Z){$.startVector(8,Z.length,8);for(let Q=Z.length-1;Q>=0;Q--)$.addInt64(Z[Q]);return $.endVector()}static startFeaturesVector($,Z){$.startVector(8,Z,8)}static endSchema($){return $.endObject()}static createSchema($,Z,Q,X,J){return _$.startSchema($),_$.addEndianness($,Z),_$.addFields($,Q),_$.addCustomMetadata($,X),_$.addFeatures($,J),_$.endSchema($)}}var bK=O(()=>{y0();VK();hK();s6()});var T0;var mK=O(()=>{(function($){$[$.NONE=0]="NONE",$[$.Schema=1]="Schema",$[$.DictionaryBatch=2]="DictionaryBatch",$[$.RecordBatch=3]="RecordBatch",$[$.Tensor=4]="Tensor",$[$.SparseTensor=5]="SparseTensor"})(T0||(T0={}))});var A,aZ;var u$=O(()=>{Y4();FK();NK();UK();UY();MK();mK();(function($){$[$.NONE=0]="NONE",$[$.Null=1]="Null",$[$.Int=2]="Int",$[$.Float=3]="Float",$[$.Binary=4]="Binary",$[$.Utf8=5]="Utf8",$[$.Bool=6]="Bool",$[$.Decimal=7]="Decimal",$[$.Date=8]="Date",$[$.Time=9]="Time",$[$.Timestamp=10]="Timestamp",$[$.Interval=11]="Interval",$[$.List=12]="List",$[$.Struct=13]="Struct",$[$.Union=14]="Union",$[$.FixedSizeBinary=15]="FixedSizeBinary",$[$.FixedSizeList=16]="FixedSizeList",$[$.Map=17]="Map",$[$.Duration=18]="Duration",$[$.LargeBinary=19]="LargeBinary",$[$.LargeUtf8=20]="LargeUtf8",$[$.BinaryView=23]="BinaryView",$[$.Utf8View=24]="Utf8View",$[$.Dictionary=-1]="Dictionary",$[$.Int8=-2]="Int8",$[$.Int16=-3]="Int16",$[$.Int32=-4]="Int32",$[$.Int64=-5]="Int64",$[$.Uint8=-6]="Uint8",$[$.Uint16=-7]="Uint16",$[$.Uint32=-8]="Uint32",$[$.Uint64=-9]="Uint64",$[$.Float16=-10]="Float16",$[$.Float32=-11]="Float32",$[$.Float64=-12]="Float64",$[$.DateDay=-13]="DateDay",$[$.DateMillisecond=-14]="DateMillisecond",$[$.TimestampSecond=-15]="TimestampSecond",$[$.TimestampMillisecond=-16]="TimestampMillisecond",$[$.TimestampMicrosecond=-17]="TimestampMicrosecond",$[$.TimestampNanosecond=-18]="TimestampNanosecond",$[$.TimeSecond=-19]="TimeSecond",$[$.TimeMillisecond=-20]="TimeMillisecond",$[$.TimeMicrosecond=-21]="TimeMicrosecond",$[$.TimeNanosecond=-22]="TimeNanosecond",$[$.DenseUnion=-23]="DenseUnion",$[$.SparseUnion=-24]="SparseUnion",$[$.IntervalDayTime=-25]="IntervalDayTime",$[$.IntervalYearMonth=-26]="IntervalYearMonth",$[$.DurationSecond=-27]="DurationSecond",$[$.DurationMillisecond=-28]="DurationMillisecond",$[$.DurationMicrosecond=-29]="DurationMicrosecond",$[$.DurationNanosecond=-30]="DurationNanosecond",$[$.IntervalMonthDayNano=-31]="IntervalMonthDayNano"})(A||(A={}));(function($){$[$.OFFSET=0]="OFFSET",$[$.DATA=1]="DATA",$[$.VALIDITY=2]="VALIDITY",$[$.TYPE=3]="TYPE"})(aZ||(aZ={}))});var fK={};P8(fK,{valueToString:()=>A8});function A8($){if($===null)return"null";if($===void 0)return"undefined";switch(typeof $){case"number":return`${$}`;case"bigint":return`${$}`;case"string":return`"${$}"`}if(typeof $[Symbol.toPrimitive]==="function")return $[Symbol.toPrimitive]("string");if(ArrayBuffer.isView($)){if($ instanceof BigInt64Array||$ instanceof BigUint64Array)return`[${[...$].map((Z)=>A8(Z))}]`;return`[${$}]`}return ArrayBuffer.isView($)?`[${$}]`:JSON.stringify($,(Z,Q)=>typeof Q==="bigint"?`${Q}`:Q)}function c0($){if(typeof $==="bigint"&&($Number.MAX_SAFE_INTEGER))throw TypeError(`${$} is not safe to convert to a number.`);return Number($)}function uK($,Z){return c0($/Z)+c0($%Z)/c0(Z)}var lK={};P8(lK,{isArrowBigNumSymbol:()=>f9,bigNumToString:()=>a6,bigNumToNumber:()=>cK,bigNumToBigInt:()=>u9,BN:()=>VY});function D8($,...Z){if(Z.length===0)return Object.setPrototypeOf(S0(this.TypedArray,$),this.constructor.prototype);return Object.setPrototypeOf(new this.TypedArray($,...Z),this.constructor.prototype)}function i6(...$){return D8.apply(this,$)}function r6(...$){return D8.apply(this,$)}function jY(...$){return D8.apply(this,$)}function cK($,Z){let{buffer:Q,byteOffset:X,byteLength:J,signed:W}=$,Y=new BigUint64Array(Q,X,J/8),G=W&&Y.at(-1)&BigInt(1)<0){let q=BigInt("1".padEnd(Z+1,"0")),P=H/q,U=G?-(H%q):H%q,M=c0(P),L=`${U}`.padStart(Z,"0");return+`${G&&M===0?"-":""}${M}.${L}`}return c0(H)}function a6($){if($.byteLength===8)return`${new $.BigIntArray($.buffer,$.byteOffset,1)[0]}`;if(!$.signed)return pK($);let Z=new Uint16Array($.buffer,$.byteOffset,$.byteLength/2);if(new Int16Array([Z.at(-1)])[0]>=0)return pK($);Z=Z.slice();let X=1;for(let W=0;W{s$();f9=Symbol.for("isArrowBigNum");D8.prototype[f9]=!0;D8.prototype.toJSON=function(){return`"${a6(this)}"`};D8.prototype.valueOf=function($){return cK(this,$)};D8.prototype.toString=function(){return a6(this)};D8.prototype[Symbol.toPrimitive]=function($="default"){switch($){case"number":return cK(this);case"string":return a6(this);case"default":return u9(this)}return a6(this)};Object.setPrototypeOf(i6.prototype,Object.create(Int32Array.prototype));Object.setPrototypeOf(r6.prototype,Object.create(Uint32Array.prototype));Object.setPrototypeOf(jY.prototype,Object.create(Uint32Array.prototype));Object.assign(i6.prototype,D8.prototype,{constructor:i6,signed:!0,TypedArray:Int32Array,BigIntArray:BigInt64Array});Object.assign(r6.prototype,D8.prototype,{constructor:r6,signed:!1,TypedArray:Uint32Array,BigIntArray:BigUint64Array});Object.assign(jY.prototype,D8.prototype,{constructor:jY,signed:!0,TypedArray:Uint32Array,BigIntArray:BigUint64Array});iD=BigInt(4294967296)*BigInt(4294967296),rD=iD-BigInt(1)});class k{static isDataType($){return($===null||$===void 0?void 0:$[Pq])===!0}static isNull($){return($===null||$===void 0?void 0:$.typeId)===A.Null}static isInt($){return($===null||$===void 0?void 0:$.typeId)===A.Int}static isFloat($){return($===null||$===void 0?void 0:$.typeId)===A.Float}static isBinary($){return($===null||$===void 0?void 0:$.typeId)===A.Binary}static isBinaryView($){return($===null||$===void 0?void 0:$.typeId)===A.BinaryView}static isLargeBinary($){return($===null||$===void 0?void 0:$.typeId)===A.LargeBinary}static isUtf8($){return($===null||$===void 0?void 0:$.typeId)===A.Utf8}static isUtf8View($){return($===null||$===void 0?void 0:$.typeId)===A.Utf8View}static isLargeUtf8($){return($===null||$===void 0?void 0:$.typeId)===A.LargeUtf8}static isBool($){return($===null||$===void 0?void 0:$.typeId)===A.Bool}static isDecimal($){return($===null||$===void 0?void 0:$.typeId)===A.Decimal}static isDate($){return($===null||$===void 0?void 0:$.typeId)===A.Date}static isTime($){return($===null||$===void 0?void 0:$.typeId)===A.Time}static isTimestamp($){return($===null||$===void 0?void 0:$.typeId)===A.Timestamp}static isInterval($){return($===null||$===void 0?void 0:$.typeId)===A.Interval}static isDuration($){return($===null||$===void 0?void 0:$.typeId)===A.Duration}static isList($){return($===null||$===void 0?void 0:$.typeId)===A.List}static isStruct($){return($===null||$===void 0?void 0:$.typeId)===A.Struct}static isUnion($){return($===null||$===void 0?void 0:$.typeId)===A.Union}static isFixedSizeBinary($){return($===null||$===void 0?void 0:$.typeId)===A.FixedSizeBinary}static isFixedSizeList($){return($===null||$===void 0?void 0:$.typeId)===A.FixedSizeList}static isMap($){return($===null||$===void 0?void 0:$.typeId)===A.Map}static isDictionary($){return($===null||$===void 0?void 0:$.typeId)===A.Dictionary}static isDenseUnion($){return k.isUnion($)&&$.mode===m0.Dense}static isSparseUnion($){return k.isUnion($)&&$.mode===m0.Sparse}constructor($){this.typeId=$}}function RQ($){let Z=$;switch($.typeId){case A.Decimal:return $.bitWidth/32;case A.Interval:{if(Z.unit===s0.MONTH_DAY_NANO)return 4;return 1+Z.unit}case A.BinaryView:case A.Utf8View:return 16;case A.FixedSizeList:return Z.listSize;case A.FixedSizeBinary:return Z.byteWidth;default:return 1}}var p9,c9,l9,d9,n9,o9,s9,i9,r9,a9,t9,e9,$q,Zq,Qq,Xq,Jq,Wq,Yq,Gq,Kq,Hq,zq,qq,Pq,j$,i$,s8,i8,GZ,AZ,r8,a8,t8,VQ,iQ,t6,e8,$Q,xZ,R0,rQ,T$,OQ,aQ,V$,tQ,O8,C8,tZ,RY,j8,V8,LZ,j0,CQ,EY,HJ,eQ,jQ,eZ,aD,YZ;var H$=O(()=>{u$();Pq=Symbol.for("apache-arrow/DataType");p9=Symbol.toStringTag;k[p9]=(($)=>{return $.children=null,$.ArrayType=Array,$.OffsetArrayType=Int32Array,$[Pq]=!0,$[Symbol.toStringTag]="DataType"})(k.prototype);j$=class j$ extends k{constructor(){super(A.Null)}toString(){return"Null"}};c9=Symbol.toStringTag;j$[c9]=(($)=>$[Symbol.toStringTag]="Null")(j$.prototype);i$=class i$ extends k{constructor($,Z){super(A.Int);this.isSigned=$,this.bitWidth=Z}get ArrayType(){switch(this.bitWidth){case 8:return this.isSigned?Int8Array:Uint8Array;case 16:return this.isSigned?Int16Array:Uint16Array;case 32:return this.isSigned?Int32Array:Uint32Array;case 64:return this.isSigned?BigInt64Array:BigUint64Array}throw Error(`Unrecognized ${this[Symbol.toStringTag]} type`)}toString(){return`${this.isSigned?"I":"Ui"}nt${this.bitWidth}`}};l9=Symbol.toStringTag;i$[l9]=(($)=>{return $.isSigned=null,$.bitWidth=null,$[Symbol.toStringTag]="Int"})(i$.prototype);s8=class s8 extends i${constructor(){super(!0,8)}get ArrayType(){return Int8Array}};i8=class i8 extends i${constructor(){super(!0,16)}get ArrayType(){return Int16Array}};GZ=class GZ extends i${constructor(){super(!0,32)}get ArrayType(){return Int32Array}};AZ=class AZ extends i${constructor(){super(!0,64)}get ArrayType(){return BigInt64Array}};r8=class r8 extends i${constructor(){super(!1,8)}get ArrayType(){return Uint8Array}};a8=class a8 extends i${constructor(){super(!1,16)}get ArrayType(){return Uint16Array}};t8=class t8 extends i${constructor(){super(!1,32)}get ArrayType(){return Uint32Array}};VQ=class VQ extends i${constructor(){super(!1,64)}get ArrayType(){return BigUint64Array}};Object.defineProperty(s8.prototype,"ArrayType",{value:Int8Array});Object.defineProperty(i8.prototype,"ArrayType",{value:Int16Array});Object.defineProperty(GZ.prototype,"ArrayType",{value:Int32Array});Object.defineProperty(AZ.prototype,"ArrayType",{value:BigInt64Array});Object.defineProperty(r8.prototype,"ArrayType",{value:Uint8Array});Object.defineProperty(a8.prototype,"ArrayType",{value:Uint16Array});Object.defineProperty(t8.prototype,"ArrayType",{value:Uint32Array});Object.defineProperty(VQ.prototype,"ArrayType",{value:BigUint64Array});iQ=class iQ extends k{constructor($){super(A.Float);this.precision=$}get ArrayType(){switch(this.precision){case J$.HALF:return Uint16Array;case J$.SINGLE:return Float32Array;case J$.DOUBLE:return Float64Array}throw Error(`Unrecognized ${this[Symbol.toStringTag]} type`)}toString(){return`Float${this.precision<<5||16}`}};d9=Symbol.toStringTag;iQ[d9]=(($)=>{return $.precision=null,$[Symbol.toStringTag]="Float"})(iQ.prototype);t6=class t6 extends iQ{constructor(){super(J$.HALF)}};e8=class e8 extends iQ{constructor(){super(J$.SINGLE)}};$Q=class $Q extends iQ{constructor(){super(J$.DOUBLE)}};Object.defineProperty(t6.prototype,"ArrayType",{value:Uint16Array});Object.defineProperty(e8.prototype,"ArrayType",{value:Float32Array});Object.defineProperty($Q.prototype,"ArrayType",{value:Float64Array});xZ=class xZ extends k{constructor(){super(A.Binary)}toString(){return"Binary"}};n9=Symbol.toStringTag;xZ[n9]=(($)=>{return $.ArrayType=Uint8Array,$[Symbol.toStringTag]="Binary"})(xZ.prototype);R0=class R0 extends k{constructor(){super(A.BinaryView)}toString(){return"BinaryView"}};o9=Symbol.toStringTag;R0.ELEMENT_WIDTH=16;R0.INLINE_CAPACITY=12;R0.LENGTH_OFFSET=0;R0.INLINE_OFFSET=4;R0.BUFFER_INDEX_OFFSET=8;R0.BUFFER_OFFSET_OFFSET=12;R0[o9]=(($)=>{return $.ArrayType=Uint8Array,$[Symbol.toStringTag]="BinaryView"})(R0.prototype);rQ=class rQ extends k{constructor(){super(A.LargeBinary)}toString(){return"LargeBinary"}};s9=Symbol.toStringTag;rQ[s9]=(($)=>{return $.ArrayType=Uint8Array,$.OffsetArrayType=BigInt64Array,$[Symbol.toStringTag]="LargeBinary"})(rQ.prototype);T$=class T$ extends k{constructor(){super(A.Utf8)}toString(){return"Utf8"}};i9=Symbol.toStringTag;T$[i9]=(($)=>{return $.ArrayType=Uint8Array,$[Symbol.toStringTag]="Utf8"})(T$.prototype);OQ=class OQ extends k{constructor(){super(A.Utf8View)}toString(){return"Utf8View"}};r9=Symbol.toStringTag;OQ.ELEMENT_WIDTH=R0.ELEMENT_WIDTH;OQ.INLINE_CAPACITY=R0.INLINE_CAPACITY;OQ[r9]=(($)=>{return $.ArrayType=Uint8Array,$[Symbol.toStringTag]="Utf8View"})(OQ.prototype);aQ=class aQ extends k{constructor(){super(A.LargeUtf8)}toString(){return"LargeUtf8"}};a9=Symbol.toStringTag;aQ[a9]=(($)=>{return $.ArrayType=Uint8Array,$.OffsetArrayType=BigInt64Array,$[Symbol.toStringTag]="LargeUtf8"})(aQ.prototype);V$=class V$ extends k{constructor(){super(A.Bool)}toString(){return"Bool"}};t9=Symbol.toStringTag;V$[t9]=(($)=>{return $.ArrayType=Uint8Array,$[Symbol.toStringTag]="Bool"})(V$.prototype);tQ=class tQ extends k{constructor($,Z,Q=128){super(A.Decimal);this.scale=$,this.precision=Z,this.bitWidth=Q}toString(){return`Decimal[${this.precision}e${this.scale>0?"+":""}${this.scale}]`}};e9=Symbol.toStringTag;tQ[e9]=(($)=>{return $.scale=null,$.precision=null,$.ArrayType=Uint32Array,$[Symbol.toStringTag]="Decimal"})(tQ.prototype);O8=class O8 extends k{constructor($){super(A.Date);this.unit=$}toString(){return`Date${(this.unit+1)*32}<${r0[this.unit]}>`}get ArrayType(){return this.unit===r0.DAY?Int32Array:BigInt64Array}};$q=Symbol.toStringTag;O8[$q]=(($)=>{return $.unit=null,$[Symbol.toStringTag]="Date"})(O8.prototype);C8=class C8 extends k{constructor($,Z){super(A.Time);this.unit=$,this.bitWidth=Z}toString(){return`Time${this.bitWidth}<${l[this.unit]}>`}get ArrayType(){switch(this.bitWidth){case 32:return Int32Array;case 64:return BigInt64Array}throw Error(`Unrecognized ${this[Symbol.toStringTag]} type`)}};Zq=Symbol.toStringTag;C8[Zq]=(($)=>{return $.unit=null,$.bitWidth=null,$[Symbol.toStringTag]="Time"})(C8.prototype);tZ=class tZ extends k{constructor($,Z){super(A.Timestamp);this.unit=$,this.timezone=Z}toString(){return`Timestamp<${l[this.unit]}${this.timezone?`, ${this.timezone}`:""}>`}};Qq=Symbol.toStringTag;tZ[Qq]=(($)=>{return $.unit=null,$.timezone=null,$.ArrayType=BigInt64Array,$[Symbol.toStringTag]="Timestamp"})(tZ.prototype);RY=class RY extends tZ{constructor($){super(l.MILLISECOND,$)}};j8=class j8 extends k{constructor($){super(A.Interval);this.unit=$}toString(){return`Interval<${s0[this.unit]}>`}};Xq=Symbol.toStringTag;j8[Xq]=(($)=>{return $.unit=null,$.ArrayType=Int32Array,$[Symbol.toStringTag]="Interval"})(j8.prototype);V8=class V8 extends k{constructor($){super(A.Duration);this.unit=$}toString(){return`Duration<${l[this.unit]}>`}};Jq=Symbol.toStringTag;V8[Jq]=(($)=>{return $.unit=null,$.ArrayType=BigInt64Array,$[Symbol.toStringTag]="Duration"})(V8.prototype);LZ=class LZ extends k{constructor($){super(A.List);this.children=[$]}toString(){return`List<${this.valueType}>`}get valueType(){return this.children[0].type}get valueField(){return this.children[0]}get ArrayType(){return this.valueType.ArrayType}};Wq=Symbol.toStringTag;LZ[Wq]=(($)=>{return $.children=null,$[Symbol.toStringTag]="List"})(LZ.prototype);j0=class j0 extends k{constructor($){super(A.Struct);this.children=$}toString(){return`Struct<{${this.children.map(($)=>`${$.name}:${$.type}`).join(", ")}}>`}};Yq=Symbol.toStringTag;j0[Yq]=(($)=>{return $.children=null,$[Symbol.toStringTag]="Struct"})(j0.prototype);CQ=class CQ extends k{constructor($,Z,Q){super(A.Union);this.mode=$,this.children=Q,this.typeIds=Z=Int32Array.from(Z),this.typeIdToChildIndex=Z.reduce((X,J,W)=>(X[J]=W)&&X||X,Object.create(null))}toString(){return`${this[Symbol.toStringTag]}<${this.children.map(($)=>`${$.type}`).join(" | ")}>`}};Gq=Symbol.toStringTag;CQ[Gq]=(($)=>{return $.mode=null,$.typeIds=null,$.children=null,$.typeIdToChildIndex=null,$.ArrayType=Int8Array,$[Symbol.toStringTag]="Union"})(CQ.prototype);EY=class EY extends CQ{constructor($,Z){super(m0.Dense,$,Z)}};HJ=class HJ extends CQ{constructor($,Z){super(m0.Sparse,$,Z)}};eQ=class eQ extends k{constructor($){super(A.FixedSizeBinary);this.byteWidth=$}toString(){return`FixedSizeBinary[${this.byteWidth}]`}};Kq=Symbol.toStringTag;eQ[Kq]=(($)=>{return $.byteWidth=null,$.ArrayType=Uint8Array,$[Symbol.toStringTag]="FixedSizeBinary"})(eQ.prototype);jQ=class jQ extends k{constructor($,Z){super(A.FixedSizeList);this.listSize=$,this.children=[Z]}get valueType(){return this.children[0].type}get valueField(){return this.children[0]}get ArrayType(){return this.valueType.ArrayType}toString(){return`FixedSizeList[${this.listSize}]<${this.valueType}>`}};Hq=Symbol.toStringTag;jQ[Hq]=(($)=>{return $.children=null,$.listSize=null,$[Symbol.toStringTag]="FixedSizeList"})(jQ.prototype);eZ=class eZ extends k{constructor($,Z=!1){var Q,X,J;super(A.Map);if(this.children=[$],this.keysSorted=Z,$){if($.name="entries",(Q=$===null||$===void 0?void 0:$.type)===null||Q===void 0?void 0:Q.children){let W=(X=$===null||$===void 0?void 0:$.type)===null||X===void 0?void 0:X.children[0];if(W)W.name="key";let Y=(J=$===null||$===void 0?void 0:$.type)===null||J===void 0?void 0:J.children[1];if(Y)Y.name="value"}}}get keyType(){return this.children[0].type.children[0].type}get valueType(){return this.children[0].type.children[1].type}get childType(){return this.children[0].type}toString(){return`Map<{${this.children[0].type.children.map(($)=>`${$.name}:${$.type}`).join(", ")}}>`}};zq=Symbol.toStringTag;eZ[zq]=(($)=>{return $.children=null,$.keysSorted=null,$[Symbol.toStringTag]="Map_"})(eZ.prototype);aD=(($)=>()=>++$)(-1);YZ=class YZ extends k{constructor($,Z,Q,X){super(A.Dictionary);this.indices=Z,this.dictionary=$,this.isOrdered=X||!1,this.id=Q==null?aD():c0(Q)}get children(){return this.dictionary.children}get valueType(){return this.dictionary}get ArrayType(){return this.dictionary.ArrayType}toString(){return`Dictionary<${this.indices}, ${this.dictionary}>`}};qq=Symbol.toStringTag;YZ[qq]=(($)=>{return $.id=null,$.indices=null,$.isOrdered=null,$.dictionary=null,$[Symbol.toStringTag]="Dictionary"})(YZ.prototype)});class H0{visitMany($,...Z){return $.map((Q,X)=>this.visit(Q,...Z.map((J)=>J[X])))}visit(...$){return this.getVisitFn($[0],!1).apply(this,$)}getVisitFn($,Z=!0){return tD(this,$,Z)}getVisitFnByTypeId($,Z=!0){return e6(this,$,Z)}visitNull($,...Z){return null}visitBool($,...Z){return null}visitInt($,...Z){return null}visitFloat($,...Z){return null}visitUtf8($,...Z){return null}visitLargeUtf8($,...Z){return null}visitUtf8View($,...Z){return null}visitBinary($,...Z){return null}visitLargeBinary($,...Z){return null}visitBinaryView($,...Z){return null}visitFixedSizeBinary($,...Z){return null}visitDate($,...Z){return null}visitTimestamp($,...Z){return null}visitTime($,...Z){return null}visitDecimal($,...Z){return null}visitList($,...Z){return null}visitStruct($,...Z){return null}visitUnion($,...Z){return null}visitDictionary($,...Z){return null}visitInterval($,...Z){return null}visitDuration($,...Z){return null}visitFixedSizeList($,...Z){return null}visitMap($,...Z){return null}}function tD($,Z,Q=!0){if(typeof Z==="number")return e6($,Z,Q);if(typeof Z==="string"&&Z in A)return e6($,A[Z],Q);if(Z&&Z instanceof k)return e6($,Nq(Z),Q);if((Z===null||Z===void 0?void 0:Z.type)&&Z.type instanceof k)return e6($,Nq(Z.type),Q);return e6($,A.NONE,Q)}function e6($,Z,Q=!0){let X=null;switch(Z){case A.Null:X=$.visitNull;break;case A.Bool:X=$.visitBool;break;case A.Int:X=$.visitInt;break;case A.Int8:X=$.visitInt8||$.visitInt;break;case A.Int16:X=$.visitInt16||$.visitInt;break;case A.Int32:X=$.visitInt32||$.visitInt;break;case A.Int64:X=$.visitInt64||$.visitInt;break;case A.Uint8:X=$.visitUint8||$.visitInt;break;case A.Uint16:X=$.visitUint16||$.visitInt;break;case A.Uint32:X=$.visitUint32||$.visitInt;break;case A.Uint64:X=$.visitUint64||$.visitInt;break;case A.Float:X=$.visitFloat;break;case A.Float16:X=$.visitFloat16||$.visitFloat;break;case A.Float32:X=$.visitFloat32||$.visitFloat;break;case A.Float64:X=$.visitFloat64||$.visitFloat;break;case A.Utf8:X=$.visitUtf8;break;case A.LargeUtf8:X=$.visitLargeUtf8;break;case A.Utf8View:X=$.visitUtf8View||$.visitUtf8;break;case A.Binary:X=$.visitBinary;break;case A.LargeBinary:X=$.visitLargeBinary;break;case A.BinaryView:X=$.visitBinaryView||$.visitBinary;break;case A.FixedSizeBinary:X=$.visitFixedSizeBinary;break;case A.Date:X=$.visitDate;break;case A.DateDay:X=$.visitDateDay||$.visitDate;break;case A.DateMillisecond:X=$.visitDateMillisecond||$.visitDate;break;case A.Timestamp:X=$.visitTimestamp;break;case A.TimestampSecond:X=$.visitTimestampSecond||$.visitTimestamp;break;case A.TimestampMillisecond:X=$.visitTimestampMillisecond||$.visitTimestamp;break;case A.TimestampMicrosecond:X=$.visitTimestampMicrosecond||$.visitTimestamp;break;case A.TimestampNanosecond:X=$.visitTimestampNanosecond||$.visitTimestamp;break;case A.Time:X=$.visitTime;break;case A.TimeSecond:X=$.visitTimeSecond||$.visitTime;break;case A.TimeMillisecond:X=$.visitTimeMillisecond||$.visitTime;break;case A.TimeMicrosecond:X=$.visitTimeMicrosecond||$.visitTime;break;case A.TimeNanosecond:X=$.visitTimeNanosecond||$.visitTime;break;case A.Decimal:X=$.visitDecimal;break;case A.List:X=$.visitList;break;case A.Struct:X=$.visitStruct;break;case A.Union:X=$.visitUnion;break;case A.DenseUnion:X=$.visitDenseUnion||$.visitUnion;break;case A.SparseUnion:X=$.visitSparseUnion||$.visitUnion;break;case A.Dictionary:X=$.visitDictionary;break;case A.Interval:X=$.visitInterval;break;case A.IntervalDayTime:X=$.visitIntervalDayTime||$.visitInterval;break;case A.IntervalYearMonth:X=$.visitIntervalYearMonth||$.visitInterval;break;case A.IntervalMonthDayNano:X=$.visitIntervalMonthDayNano||$.visitInterval;break;case A.Duration:X=$.visitDuration;break;case A.DurationSecond:X=$.visitDurationSecond||$.visitDuration;break;case A.DurationMillisecond:X=$.visitDurationMillisecond||$.visitDuration;break;case A.DurationMicrosecond:X=$.visitDurationMicrosecond||$.visitDuration;break;case A.DurationNanosecond:X=$.visitDurationNanosecond||$.visitDuration;break;case A.FixedSizeList:X=$.visitFixedSizeList;break;case A.Map:X=$.visitMap;break}if(typeof X==="function")return X;if(!Q)return()=>null;throw Error(`Unrecognized type '${A[Z]}'`)}function Nq($){switch($.typeId){case A.Null:return A.Null;case A.Int:{let{bitWidth:Z,isSigned:Q}=$;switch(Z){case 8:return Q?A.Int8:A.Uint8;case 16:return Q?A.Int16:A.Uint16;case 32:return Q?A.Int32:A.Uint32;case 64:return Q?A.Int64:A.Uint64}return A.Int}case A.Float:switch($.precision){case J$.HALF:return A.Float16;case J$.SINGLE:return A.Float32;case J$.DOUBLE:return A.Float64}return A.Float;case A.Binary:return A.Binary;case A.LargeBinary:return A.LargeBinary;case A.BinaryView:return A.BinaryView;case A.Utf8:return A.Utf8;case A.LargeUtf8:return A.LargeUtf8;case A.Utf8View:return A.Utf8View;case A.Bool:return A.Bool;case A.Decimal:return A.Decimal;case A.Time:switch($.unit){case l.SECOND:return A.TimeSecond;case l.MILLISECOND:return A.TimeMillisecond;case l.MICROSECOND:return A.TimeMicrosecond;case l.NANOSECOND:return A.TimeNanosecond}return A.Time;case A.Timestamp:switch($.unit){case l.SECOND:return A.TimestampSecond;case l.MILLISECOND:return A.TimestampMillisecond;case l.MICROSECOND:return A.TimestampMicrosecond;case l.NANOSECOND:return A.TimestampNanosecond}return A.Timestamp;case A.Date:switch($.unit){case r0.DAY:return A.DateDay;case r0.MILLISECOND:return A.DateMillisecond}return A.Date;case A.Interval:switch($.unit){case s0.DAY_TIME:return A.IntervalDayTime;case s0.YEAR_MONTH:return A.IntervalYearMonth;case s0.MONTH_DAY_NANO:return A.IntervalMonthDayNano}return A.Interval;case A.Duration:switch($.unit){case l.SECOND:return A.DurationSecond;case l.MILLISECOND:return A.DurationMillisecond;case l.MICROSECOND:return A.DurationMicrosecond;case l.NANOSECOND:return A.DurationNanosecond}return A.Duration;case A.Map:return A.Map;case A.List:return A.List;case A.Struct:return A.Struct;case A.Union:switch($.mode){case m0.Dense:return A.DenseUnion;case m0.Sparse:return A.SparseUnion}return A.Union;case A.FixedSizeBinary:return A.FixedSizeBinary;case A.FixedSizeList:return A.FixedSizeList;case A.Dictionary:return A.Dictionary}throw Error(`Unrecognized type '${A[$.typeId]}'`)}var R8=O(()=>{u$();H$();H0.prototype.visitInt8=null;H0.prototype.visitInt16=null;H0.prototype.visitInt32=null;H0.prototype.visitInt64=null;H0.prototype.visitUint8=null;H0.prototype.visitUint16=null;H0.prototype.visitUint32=null;H0.prototype.visitUint64=null;H0.prototype.visitFloat16=null;H0.prototype.visitFloat32=null;H0.prototype.visitFloat64=null;H0.prototype.visitDateDay=null;H0.prototype.visitDateMillisecond=null;H0.prototype.visitTimestampSecond=null;H0.prototype.visitTimestampMillisecond=null;H0.prototype.visitTimestampMicrosecond=null;H0.prototype.visitTimestampNanosecond=null;H0.prototype.visitTimeSecond=null;H0.prototype.visitTimeMillisecond=null;H0.prototype.visitTimeMicrosecond=null;H0.prototype.visitTimeNanosecond=null;H0.prototype.visitDenseUnion=null;H0.prototype.visitSparseUnion=null;H0.prototype.visitIntervalDayTime=null;H0.prototype.visitIntervalYearMonth=null;H0.prototype.visitIntervalMonthDayNano=null;H0.prototype.visitDuration=null;H0.prototype.visitDurationSecond=null;H0.prototype.visitDurationMillisecond=null;H0.prototype.visitDurationMicrosecond=null;H0.prototype.visitDurationNanosecond=null});var nK={};P8(nK,{uint16ToFloat64:()=>z4,float64ToUint16:()=>wY});function z4($){let Z=($&31744)>>10,Q=($&1023)/1024,X=Math.pow(-1,($&32768)>>15);switch(Z){case 31:return X*(Q?Number.NaN:1/0);case 0:return X*(Q?0.00006103515625*Q:0)}return X*Math.pow(2,Z-15)*(1+Q)}function wY($){if($!==$)return 32256;Uq[0]=$;let Z=($W[1]&2147483648)>>16&65535,Q=$W[1]&2146435072,X=0;if(Q>=1089470464)if($W[0]>0)Q=31744;else Q=(Q&2080374784)>>16,X=($W[1]&1048575)>>10;else if(Q<=1056964608)X=1048576+($W[1]&1048575),X=1048576+(X<<(Q>>20)-998)>>21,Q=0;else Q=Q-1056964608>>10,X=($W[1]&1048575)+512>>10;return Z|Q|X&65535}var Uq,$W;var SY=O(()=>{Uq=new Float64Array(1),$W=new Uint32Array(Uq.buffer)});function N0($){return(Z,Q,X)=>{if(Z.setValid(Q,X!=null))return $(Z,Q,X)}}var K0,eD=($,Z,Q)=>{$[Z]=Math.floor(Q/86400000)},Mq=($,Z,Q,X)=>{if(Q+1{let J=$+Q;X?Z[J>>3]|=1<>3]&=~(1<{$[Z]=Q},oK=({values:$},Z,Q)=>{$[Z]=Q},Lq=({values:$},Z,Q)=>{$[Z]=wY(Q)},ZO=($,Z,Q)=>{switch($.type.precision){case J$.HALF:return Lq($,Z,Q);case J$.SINGLE:case J$.DOUBLE:return oK($,Z,Q)}},q4=({values:$},Z,Q)=>{eD($,Z,Q.valueOf())},P4=({values:$},Z,Q)=>{$[Z]=BigInt(Q)},sK=({stride:$,values:Z},Q,X)=>{Z.set(X.subarray(0,$),$*Q)},Aq=({values:$,valueOffsets:Z},Q,X)=>Mq($,Z,Q,X),QO=($)=>{let Z=$.variadicBuffers;if(!Array.isArray(Z)||Object.isFrozen(Z))Z=Array.from(Z),$.variadicBuffers=Z;return Z},Dq=($,Z,Q)=>{var X,J,W,Y;let G=$.values;if(!G)throw Error("BinaryView data is missing view buffer");let H=R0.ELEMENT_WIDTH,z=Z*H,q=z+H;if(z<0||q>G.length)throw RangeError(`BinaryView index ${Z} out of bounds`);G.fill(0,z,q);let P=new DataView(G.buffer,G.byteOffset+z,H),U=Q.length;if(P.setInt32(R0.LENGTH_OFFSET,U,!0),U<=R0.INLINE_CAPACITY){G.set(Q,z+R0.INLINE_OFFSET);return}let M=((X=Q[0])!==null&&X!==void 0?X:0)|((J=Q[1])!==null&&J!==void 0?J:0)<<8|((W=Q[2])!==null&&W!==void 0?W:0)<<16|((Y=Q[3])!==null&&Y!==void 0?Y:0)<<24;P.setUint32(R0.INLINE_OFFSET,M>>>0,!0);let L=QO($),D=Q.slice(),R=L.push(D)-1;P.setInt32(R0.BUFFER_INDEX_OFFSET,R,!0),P.setInt32(R0.BUFFER_OFFSET_OFFSET,0,!0)},XO=($,Z,Q)=>{let X=Q instanceof Uint8Array?Q:new Uint8Array(Q);Dq($,Z,X)},Oq=({values:$,valueOffsets:Z},Q,X)=>Mq($,Z,Q,LQ(X)),JO=($,Z,Q)=>{let X=LQ(Q);Dq($,Z,X)},iK=($,Z,Q)=>{$.type.unit===r0.DAY?q4($,Z,Q):P4($,Z,Q)},F4=({values:$},Z,Q)=>{$[Z]=BigInt(Q/1000)},N4=({values:$},Z,Q)=>{$[Z]=BigInt(Q)},U4=({values:$},Z,Q)=>{$[Z]=BigInt(Q*1000)},M4=({values:$},Z,Q)=>{$[Z]=BigInt(Q*1e6)},rK=($,Z,Q)=>{switch($.type.unit){case l.SECOND:return F4($,Z,Q);case l.MILLISECOND:return N4($,Z,Q);case l.MICROSECOND:return U4($,Z,Q);case l.NANOSECOND:return M4($,Z,Q)}},L4=({values:$},Z,Q)=>{$[Z]=Q},A4=({values:$},Z,Q)=>{$[Z]=Q},D4=({values:$},Z,Q)=>{$[Z]=Q},O4=({values:$},Z,Q)=>{$[Z]=Q},aK=($,Z,Q)=>{switch($.type.unit){case l.SECOND:return L4($,Z,Q);case l.MILLISECOND:return A4($,Z,Q);case l.MICROSECOND:return D4($,Z,Q);case l.NANOSECOND:return O4($,Z,Q)}},tK=({values:$,stride:Z},Q,X)=>{$.set(X.subarray(0,Z),Z*Q)},WO=($,Z,Q)=>{let X=$.children[0],J=$.valueOffsets,W=DZ.getVisitFn(X);if(Array.isArray(Q))for(let Y=-1,G=J[Z],H=J[Z+1];G{let X=$.children[0],{valueOffsets:J}=$,W=DZ.getVisitFn(X),{[Z]:Y,[Z+1]:G}=J,H=Q instanceof Map?Q.entries():Object.entries(Q);for(let z of H)if(W(X,Y,z),++Y>=G)break},GO=($,Z)=>(Q,X,J,W)=>X&&Q(X,$,Z[W]),KO=($,Z)=>(Q,X,J,W)=>X&&Q(X,$,Z.get(W)),HO=($,Z)=>(Q,X,J,W)=>X&&Q(X,$,Z.get(J.name)),zO=($,Z)=>(Q,X,J,W)=>X&&Q(X,$,Z[J.name]),qO=($,Z,Q)=>{let X=$.type.children.map((W)=>DZ.getVisitFn(W.type)),J=Q instanceof Map?HO(Z,Q):Q instanceof r?KO(Z,Q):Array.isArray(Q)?GO(Z,Q):zO(Z,Q);$.type.children.forEach((W,Y)=>J(X[Y],$.children[Y],W,Y))},PO=($,Z,Q)=>{$.type.mode===m0.Dense?Cq($,Z,Q):jq($,Z,Q)},Cq=($,Z,Q)=>{let X=$.type.typeIdToChildIndex[$.typeIds[Z]],J=$.children[X];DZ.visit(J,$.valueOffsets[Z],Q)},jq=($,Z,Q)=>{let X=$.type.typeIdToChildIndex[$.typeIds[Z]],J=$.children[X];DZ.visit(J,Z,Q)},FO=($,Z,Q)=>{var X;(X=$.dictionary)===null||X===void 0||X.set($.values[Z],Q)},eK=($,Z,Q)=>{switch($.type.unit){case s0.YEAR_MONTH:return j4($,Z,Q);case s0.DAY_TIME:return C4($,Z,Q);case s0.MONTH_DAY_NANO:return V4($,Z,Q)}},C4=({values:$},Z,Q)=>{$.set(Q.subarray(0,2),2*Z)},j4=({values:$},Z,Q)=>{$[Z]=Q[0]*12+Q[1]%12},V4=({values:$,stride:Z},Q,X)=>{$.set(X.subarray(0,Z),Z*Q)},R4=({values:$},Z,Q)=>{$[Z]=Q},E4=({values:$},Z,Q)=>{$[Z]=Q},w4=({values:$},Z,Q)=>{$[Z]=Q},S4=({values:$},Z,Q)=>{$[Z]=Q},$2=($,Z,Q)=>{switch($.type.unit){case l.SECOND:return R4($,Z,Q);case l.MILLISECOND:return E4($,Z,Q);case l.MICROSECOND:return w4($,Z,Q);case l.NANOSECOND:return S4($,Z,Q)}},NO=($,Z,Q)=>{let{stride:X}=$,J=$.children[0],W=DZ.getVisitFn(J);if(Array.isArray(Q))for(let Y=-1,G=Z*X;++Y{ZQ();R8();jX();SY();u$();H$();K0=class K0 extends H0{};K0.prototype.visitBool=N0($O);K0.prototype.visitInt=N0(VX);K0.prototype.visitInt8=N0(VX);K0.prototype.visitInt16=N0(VX);K0.prototype.visitInt32=N0(VX);K0.prototype.visitInt64=N0(VX);K0.prototype.visitUint8=N0(VX);K0.prototype.visitUint16=N0(VX);K0.prototype.visitUint32=N0(VX);K0.prototype.visitUint64=N0(VX);K0.prototype.visitFloat=N0(ZO);K0.prototype.visitFloat16=N0(Lq);K0.prototype.visitFloat32=N0(oK);K0.prototype.visitFloat64=N0(oK);K0.prototype.visitUtf8=N0(Oq);K0.prototype.visitLargeUtf8=N0(Oq);K0.prototype.visitUtf8View=N0(JO);K0.prototype.visitBinary=N0(Aq);K0.prototype.visitLargeBinary=N0(Aq);K0.prototype.visitBinaryView=N0(XO);K0.prototype.visitFixedSizeBinary=N0(sK);K0.prototype.visitDate=N0(iK);K0.prototype.visitDateDay=N0(q4);K0.prototype.visitDateMillisecond=N0(P4);K0.prototype.visitTimestamp=N0(rK);K0.prototype.visitTimestampSecond=N0(F4);K0.prototype.visitTimestampMillisecond=N0(N4);K0.prototype.visitTimestampMicrosecond=N0(U4);K0.prototype.visitTimestampNanosecond=N0(M4);K0.prototype.visitTime=N0(aK);K0.prototype.visitTimeSecond=N0(L4);K0.prototype.visitTimeMillisecond=N0(A4);K0.prototype.visitTimeMicrosecond=N0(D4);K0.prototype.visitTimeNanosecond=N0(O4);K0.prototype.visitDecimal=N0(tK);K0.prototype.visitList=N0(WO);K0.prototype.visitStruct=N0(qO);K0.prototype.visitUnion=N0(PO);K0.prototype.visitDenseUnion=N0(Cq);K0.prototype.visitSparseUnion=N0(jq);K0.prototype.visitDictionary=N0(FO);K0.prototype.visitInterval=N0(eK);K0.prototype.visitIntervalDayTime=N0(C4);K0.prototype.visitIntervalYearMonth=N0(j4);K0.prototype.visitIntervalMonthDayNano=N0(V4);K0.prototype.visitDuration=N0($2);K0.prototype.visitDurationSecond=N0(R4);K0.prototype.visitDurationMillisecond=N0(E4);K0.prototype.visitDurationMicrosecond=N0(w4);K0.prototype.visitDurationNanosecond=N0(S4);K0.prototype.visitFixedSizeList=N0(NO);K0.prototype.visitMap=N0(YO);DZ=new K0});class Rq{isExtensible(){return!1}deleteProperty(){return!1}preventExtensions(){return!0}ownKeys($){return $[E8].type.children.map((Z)=>Z.name)}has($,Z){return $[E8].type.children.some((Q)=>Q.name===Z)}getOwnPropertyDescriptor($,Z){if($[E8].type.children.some((Q)=>Q.name===Z))return{writable:!0,enumerable:!0,configurable:!0};return}get($,Z){if(Reflect.has($,Z))return $[Z];let Q=$[E8].type.children.findIndex((X)=>X.name===Z);if(Q!==-1){let X=p$.visit($[E8].children[Q],$[ZW]);return Reflect.set($,Z,X),X}}set($,Z,Q){let X=$[E8].type.children.findIndex((J)=>J.name===Z);if(X!==-1)return DZ.visit($[E8].children[X],$[ZW],Q),Reflect.set($,Z,Q);else if(Reflect.has($,Z)||typeof Z==="symbol")return Reflect.set($,Z,Q);return!1}}var E8,ZW,rJ,Vq,UO;var Z2=O(()=>{aJ();EQ();E8=Symbol.for("parent"),ZW=Symbol.for("rowIndex");rJ=class rJ{constructor($,Z){return this[E8]=$,this[ZW]=Z,new Proxy(this,UO)}toArray(){return Object.values(this.toJSON())}toJSON(){let $=this[ZW],Z=this[E8],Q=Z.type.children,X={};for(let J=-1,W=Q.length;++J`${A8($)}: ${A8(Z)}`).join(", ")}}`}[Symbol.for("nodejs.util.inspect.custom")](){return this.toString()}[Symbol.iterator](){return new Vq(this[E8],this[ZW])}};Vq=class Vq{constructor($,Z){this.childIndex=0,this.children=$.children,this.rowIndex=Z,this.childFields=$.type.children,this.numChildren=this.childFields.length}[Symbol.iterator](){return this}next(){let $=this.childIndex;if($Z.getValid(Q)?$(Z,Q):null}var J0,MO=($,Z)=>86400000*$[Z],QW=16,LO=12,AO=($,Z)=>null,Eq=($,Z,Q)=>{if(Q+1>=Z.length)return null;let X=c0(Z[Q]),J=c0(Z[Q+1]);return $.subarray(X,J)},DO=({offset:$,values:Z},Q)=>{let X=$+Q;return(Z[X>>3]&1<MO($,Z),Sq=({values:$},Z)=>c0($[Z]),zJ=({stride:$,values:Z},Q)=>Z[$*Q],OO=({stride:$,values:Z},Q)=>z4(Z[$*Q]),Bq=({values:$},Z)=>$[Z],CO=({stride:$,values:Z},Q)=>Z.subarray($*Q,$*(Q+1)),kq=({values:$,valueOffsets:Z},Q)=>Eq($,Z,Q),Iq=($,Z)=>{var Q;let X=$.values;if(!X)throw Error("BinaryView data is missing view buffer");let J=Z*QW,W=J+QW;if(J<0||W>X.length)throw Error(`BinaryView data buffer is too short: expected ${QW} bytes, got ${Math.max(0,X.length-J)}`);let Y=X.subarray(J,W);if(Y.length{return Iq($,Z)},_q=({values:$,valueOffsets:Z},Q)=>{let X=Eq($,Z,Q);return X!==null?n6(X):null},VO=($,Z)=>{let Q=Iq($,Z);return n6(Q)},RO=({values:$},Z)=>$[Z],EO=({type:$,values:Z},Q)=>$.precision!==J$.HALF?Z[Q]:z4(Z[Q]),wO=($,Z)=>$.type.unit===r0.DAY?wq($,Z):Sq($,Z),Tq=({values:$},Z)=>1000*c0($[Z]),gq=({values:$},Z)=>c0($[Z]),xq=({values:$},Z)=>uK($[Z],BigInt(1000)),vq=({values:$},Z)=>uK($[Z],BigInt(1e6)),SO=($,Z)=>{switch($.type.unit){case l.SECOND:return Tq($,Z);case l.MILLISECOND:return gq($,Z);case l.MICROSECOND:return xq($,Z);case l.NANOSECOND:return vq($,Z)}},yq=({values:$},Z)=>$[Z],hq=({values:$},Z)=>$[Z],bq=({values:$},Z)=>$[Z],mq=({values:$},Z)=>$[Z],BO=($,Z)=>{switch($.type.unit){case l.SECOND:return yq($,Z);case l.MILLISECOND:return hq($,Z);case l.MICROSECOND:return bq($,Z);case l.NANOSECOND:return mq($,Z)}},kO=({values:$,stride:Z},Q)=>VY.decimal($.subarray(Z*Q,Z*(Q+1))),IO=($,Z)=>{let{valueOffsets:Q,stride:X,children:J}=$,{[Z*X]:W,[Z*X+1]:Y}=Q,H=J[0].slice(W,Y-W);return new r([H])},_O=($,Z)=>{let{valueOffsets:Q,children:X}=$,{[Z]:J,[Z+1]:W}=Q,Y=X[0];return new RX(Y.slice(J,W-J))},TO=($,Z)=>{return new rJ($,Z)},gO=($,Z)=>{return $.type.mode===m0.Dense?fq($,Z):uq($,Z)},fq=($,Z)=>{let Q=$.type.typeIdToChildIndex[$.typeIds[Z]],X=$.children[Q];return p$.visit(X,$.valueOffsets[Z])},uq=($,Z)=>{let Q=$.type.typeIdToChildIndex[$.typeIds[Z]],X=$.children[Q];return p$.visit(X,Z)},xO=($,Z)=>{var Q;return(Q=$.dictionary)===null||Q===void 0?void 0:Q.get($.values[Z])},vO=($,Z)=>$.type.unit===s0.MONTH_DAY_NANO?lq($,Z):$.type.unit===s0.DAY_TIME?pq($,Z):cq($,Z),pq=({values:$},Z)=>$.subarray(2*Z,2*(Z+1)),cq=({values:$},Z)=>{let Q=$[Z],X=new Int32Array(2);return X[0]=Math.trunc(Q/12),X[1]=Math.trunc(Q%12),X},lq=({values:$},Z)=>$.subarray(4*Z,4*(Z+1)),dq=({values:$},Z)=>$[Z],nq=({values:$},Z)=>$[Z],oq=({values:$},Z)=>$[Z],sq=({values:$},Z)=>$[Z],yO=($,Z)=>{switch($.type.unit){case l.SECOND:return dq($,Z);case l.MILLISECOND:return nq($,Z);case l.MICROSECOND:return oq($,Z);case l.NANOSECOND:return sq($,Z)}},hO=($,Z)=>{let{stride:Q,children:X}=$,W=X[0].slice(Z*Q,Q);return new r([W])},p$;var aJ=O(()=>{dK();ZQ();R8();B4();Z2();jX();SY();u$();J0=class J0 extends H0{};J0.prototype.visitNull=z0(AO);J0.prototype.visitBool=z0(DO);J0.prototype.visitInt=z0(RO);J0.prototype.visitInt8=z0(zJ);J0.prototype.visitInt16=z0(zJ);J0.prototype.visitInt32=z0(zJ);J0.prototype.visitInt64=z0(Bq);J0.prototype.visitUint8=z0(zJ);J0.prototype.visitUint16=z0(zJ);J0.prototype.visitUint32=z0(zJ);J0.prototype.visitUint64=z0(Bq);J0.prototype.visitFloat=z0(EO);J0.prototype.visitFloat16=z0(OO);J0.prototype.visitFloat32=z0(zJ);J0.prototype.visitFloat64=z0(zJ);J0.prototype.visitUtf8=z0(_q);J0.prototype.visitLargeUtf8=z0(_q);J0.prototype.visitUtf8View=z0(VO);J0.prototype.visitBinary=z0(kq);J0.prototype.visitLargeBinary=z0(kq);J0.prototype.visitBinaryView=z0(jO);J0.prototype.visitFixedSizeBinary=z0(CO);J0.prototype.visitDate=z0(wO);J0.prototype.visitDateDay=z0(wq);J0.prototype.visitDateMillisecond=z0(Sq);J0.prototype.visitTimestamp=z0(SO);J0.prototype.visitTimestampSecond=z0(Tq);J0.prototype.visitTimestampMillisecond=z0(gq);J0.prototype.visitTimestampMicrosecond=z0(xq);J0.prototype.visitTimestampNanosecond=z0(vq);J0.prototype.visitTime=z0(BO);J0.prototype.visitTimeSecond=z0(yq);J0.prototype.visitTimeMillisecond=z0(hq);J0.prototype.visitTimeMicrosecond=z0(bq);J0.prototype.visitTimeNanosecond=z0(mq);J0.prototype.visitDecimal=z0(kO);J0.prototype.visitList=z0(IO);J0.prototype.visitStruct=z0(TO);J0.prototype.visitUnion=z0(gO);J0.prototype.visitDenseUnion=z0(fq);J0.prototype.visitSparseUnion=z0(uq);J0.prototype.visitDictionary=z0(xO);J0.prototype.visitInterval=z0(vO);J0.prototype.visitIntervalDayTime=z0(pq);J0.prototype.visitIntervalYearMonth=z0(cq);J0.prototype.visitIntervalMonthDayNano=z0(lq);J0.prototype.visitDuration=z0(yO);J0.prototype.visitDurationSecond=z0(dq);J0.prototype.visitDurationMillisecond=z0(nq);J0.prototype.visitDurationMicrosecond=z0(oq);J0.prototype.visitDurationNanosecond=z0(sq);J0.prototype.visitFixedSizeList=z0(hO);J0.prototype.visitMap=z0(_O);p$=new J0});class rq{isExtensible(){return!1}deleteProperty(){return!1}preventExtensions(){return!0}ownKeys($){return $[XW]}has($,Z){return $[XW].includes(Z)}getOwnPropertyDescriptor($,Z){if($[XW].indexOf(Z)!==-1)return{writable:!0,enumerable:!0,configurable:!0};return}get($,Z){if(Reflect.has($,Z))return $[Z];let Q=$[XW].indexOf(Z);if(Q!==-1){let X=p$.visit(Reflect.get($,JW),Q);return Reflect.set($,Z,X),X}}set($,Z,Q){let X=$[XW].indexOf(Z);if(X!==-1)return DZ.visit(Reflect.get($,JW),X,Q),Reflect.set($,Z,Q);else if(Reflect.has($,Z))return Reflect.set($,Z,Q);return!1}}var qJ,JW,XW,Q2,RX,iq;var B4=O(()=>{ZQ();aJ();EQ();qJ=Symbol.for("keys"),JW=Symbol.for("vals"),XW=Symbol.for("kKeysAsStrings"),Q2=Symbol.for("_kKeysAsStrings");RX=class RX{constructor($){return this[qJ]=new r([$.children[0]]).memoize(),this[JW]=$.children[1],new Proxy(this,new rq)}get[XW](){return this[Q2]||(this[Q2]=Array.from(this[qJ].toArray(),String))}[Symbol.iterator](){return new iq(this[qJ],this[JW])}get size(){return this[qJ].length}toArray(){return Object.values(this.toJSON())}toJSON(){let $=this[qJ],Z=this[JW],Q={};for(let X=-1,J=$.length;++X`${A8($)}: ${A8(Z)}`).join(", ")}}`}[Symbol.for("nodejs.util.inspect.custom")](){return this.toString()}};iq=class iq{constructor($,Z){this.keys=$,this.vals=Z,this.keyIndex=0,this.numKeys=$.length}[Symbol.iterator](){return this}next(){let $=this.keyIndex;if($===this.numKeys)return{done:!0,value:null};return this.keyIndex++,{done:!1,value:[this.keys.get($),p$.visit(this.vals,$)]}}};Object.defineProperties(RX.prototype,{[Symbol.toStringTag]:{enumerable:!1,configurable:!1,value:"Row"},[qJ]:{writable:!0,enumerable:!1,configurable:!1,value:null},[JW]:{writable:!0,enumerable:!1,configurable:!1,value:null},[Q2]:{writable:!0,enumerable:!1,configurable:!1,value:null}})});var X2={};P8(X2,{wrapIndex:()=>tJ,createElementComparator:()=>PJ,clampRange:()=>BY});function BY($,Z,Q,X){let{length:J=0}=$,W=typeof Z!=="number"?0:Z,Y=typeof Q!=="number"?J:Q;return W<0&&(W=(W%J+J)%J),Y<0&&(Y=(Y%J+J)%J),YJ&&(Y=J),X?X($,W,Y):[W,Y]}function PJ($){if(typeof $!=="object"||$===null){if(tq($))return tq;return(Q)=>Q===$}if($ instanceof Date){let Q=$.valueOf();return(X)=>X instanceof Date?X.valueOf()===Q:!1}if(ArrayBuffer.isView($))return(Q)=>Q?zK($,Q):!1;if($ instanceof Map)return mO($);if(Array.isArray($))return bO($);if($ instanceof r)return fO($);return uO($,!0)}function bO($){let Z=[];for(let Q=-1,X=$.length;++Q!1;let X=[];for(let J=-1,W=Q.length;++J{if(!Q||typeof Q!=="object")return!1;switch(Q.constructor){case Array:return pO($,Q);case Map:return eq($,Q,Q.keys());case RX:case rJ:case Object:case void 0:return eq($,Q,Z||Object.keys(Q))}return Q instanceof r?cO($,Q):!1}}function pO($,Z){let Q=$.length;if(Z.length!==Q)return!1;for(let X=-1;++X$<0?Z+$:$,tq=($)=>$!==$;var WW=O(()=>{ZQ();B4();Z2();s$()});var J2={};P8(J2,{truncateBitmap:()=>eJ,setBool:()=>lO,popcnt_uint32:()=>I4,popcnt_bit_range:()=>kY,popcnt_array:()=>ZP,packBools:()=>$6,getBool:()=>_4,getBit:()=>$P,BitIterator:()=>YW});function _4($,Z,Q,X){return(Q&1<>X}function lO($,Z,Q){return Q?!!($[Z>>3]|=1<>3]&=~(1<0||Q.byteLength>3):$6(new YW(Q,$,Z,null,_4)).subarray(0,X)),J}return Q}function $6($){let Z=[],Q=0,X=0,J=0;for(let Y of $)if(Y&&(J|=1<0)Z[Q++]=J;let W=new Uint8Array(Z.length+7&-8);return W.set(Z),W}function kY($,Z,Q){if(Q-Z<=0)return 0;if(Q-Z<8){let W=0;for(let Y of new YW($,Z,Q-Z,$,$P))W+=Y;return W}let X=Q>>3<<3,J=Z+(Z%8===0?0:8-Z%8);return kY($,Z,J)+kY($,X,Q)+ZP($,J>>3,X-J>>3)}function ZP($,Z,Q){let X=0,J=Math.trunc(Z),W=new DataView($.buffer,$.byteOffset,$.byteLength),Y=Q===void 0?$.byteLength:J+Q;while(Y-J>=4)X+=I4(W.getUint32(J)),J+=4;while(Y-J>=2)X+=I4(W.getUint16(J)),J+=2;while(Y-J>=1)X+=I4(W.getUint8(J)),J+=1;return X}function I4($){let Z=Math.trunc($);return Z=Z-(Z>>>1&1431655765),Z=(Z&858993459)+(Z>>>2&858993459),(Z+(Z>>>4)&252645135)*16843009>>>24}var YW;var GW=O(()=>{YW=class YW{constructor($,Z,Q,X,J){this.bytes=$,this.length=Q,this.context=X,this.get=J,this.bit=Z%8,this.byteIndex=Z>>3,this.byte=$[this.byteIndex++],this.index=0}next(){if(this.indexZ.nullable);else if(k.isDenseUnion($))return this.children.some((Z)=>Z.nullable);return this.nullBitmap&&this.nullBitmap.byteLength>0}return!0}get byteLength(){let $=0,{valueOffsets:Z,values:Q,nullBitmap:X,typeIds:J}=this;return Z&&($+=Z.byteLength),Q&&($+=Q.byteLength),X&&($+=X.byteLength),J&&($+=J.byteLength),$+=this.variadicBuffers.reduce((W,Y)=>{var G;return W+((G=Y===null||Y===void 0?void 0:Y.byteLength)!==null&&G!==void 0?G:0)},0),this.children.reduce((W,Y)=>W+Y.byteLength,$)}get nullCount(){if(k.isUnion(this.type))return this.children.reduce((Q,X)=>Q+X.nullCount,0);let $=this._nullCount,Z;if($<=dO&&(Z=this.nullBitmap))this._nullCount=$=Z.length===0?0:this.length-kY(Z,this.offset,this.offset+this.length);return $}constructor($,Z,Q,X,J,W=[],Y,G=[]){var H;this.type=$,this.children=W,this.dictionary=Y,this.offset=Math.floor(Math.max(Z||0,0)),this.length=Math.floor(Math.max(Q||0,0)),this._nullCount=Math.floor(Math.max(X||0,-1));let z;if(J instanceof A0)this.stride=J.stride,this.values=J.values,this.typeIds=J.typeIds,this.nullBitmap=J.nullBitmap,this.valueOffsets=J.valueOffsets,this.variadicBuffers=J.variadicBuffers;else{if(this.stride=RQ($),J)(z=J[0])&&(this.valueOffsets=z),(z=J[1])&&(this.values=z),(z=J[2])&&(this.nullBitmap=z),(z=J[3])&&(this.typeIds=z);this.variadicBuffers=G}(H=this.variadicBuffers)!==null&&H!==void 0||(this.variadicBuffers=[])}getValid($){let{type:Z}=this;if(k.isUnion(Z)){let Q=Z,X=this.typeIds[$],J=Q.typeIdToChildIndex[X],W=this.children[J],Y=this.valueOffsets,G=Q.mode===m0.Dense&&Y?Number(Y[$]):$;return W.getValid(G)}if(this.nullable&&this.nullCount>0){let Q=this.offset+$;return(this.nullBitmap[Q>>3]&1<>3;if(!J||J.byteLength<=z)if(J=new Uint8Array((W+Y+63&-64)>>3).fill(255),this.nullCount>0)J.set(eJ(W,Y,this.nullBitmap),0),Object.assign(this,{nullBitmap:J});else Object.assign(this,{nullBitmap:J,_nullCount:0});let q=J[z];Q=(q&H)!==0,J[z]=Z?q|H:q&~H}if(Q!==!!Z)this._nullCount=this.nullCount+(Z?-1:1);return Z}clone($=this.type,Z=this.offset,Q=this.length,X=this._nullCount,J=this,W=this.children,Y=this.variadicBuffers){return new A0($,Z,Q,X,J,W,this.dictionary,Y)}slice($,Z){let{stride:Q,typeId:X,children:J}=this,W=+(this._nullCount===0)-1,Y=X===16?Q:1,G=this._sliceBuffers($,Z,Q,X);return this.clone(this.type,this.offset+$,Z,W,G,J.length===0||this.valueOffsets?J:this._sliceChildren(J,Y*$,Y*Z),this.variadicBuffers)}_changeLengthAndBackfillNullBitmap($){if(this.typeId===A.Null)return this.clone(this.type,0,$,0,this.buffers,this.children,this.variadicBuffers);let{length:Z,nullCount:Q}=this,X=new Uint8Array(($+63&-64)>>3).fill(255,0,Z>>3);if(X[Z>>3]=(1<0)X.set(eJ(this.offset,Z,this.nullBitmap),0);let J=this.buffers;return J[aZ.VALIDITY]=X,this.clone(this.type,0,$,Q+($-Z),J,this.children,this.variadicBuffers)}_sliceBuffers($,Z,Q,X){let J,{buffers:W}=this;if((J=W[aZ.TYPE])&&(W[aZ.TYPE]=J.subarray($,$+Z)),k.isBinaryView(this.type)||k.isUtf8View(this.type)){let Y=R0.ELEMENT_WIDTH;(J=W[aZ.DATA])&&(W[aZ.DATA]=J.subarray($*Y,($+Z)*Y))}else(J=W[aZ.OFFSET])&&(W[aZ.OFFSET]=J.subarray($,$+Z+1))||(J=W[aZ.DATA])&&(W[aZ.DATA]=X===6?J:J.subarray(Q*$,Q*($+Z)));return W}_sliceChildren($,Z,Q){return $.map((X)=>X.slice(Z,Q))}}function I($){return nO.visit($)}var dO=-1,QP,IY,nO;var $8=O(()=>{ZQ();u$();H$();GW();H$();R8();s$();QP=Symbol.for("apache-arrow/Data");A0.prototype.children=Object.freeze([]);A0.prototype[QP]=!0;Object.defineProperty(A0,Symbol.hasInstance,{value:function(Z){return Function.prototype[Symbol.hasInstance].call(this,Z)||this===A0&&A0.isData(Z)}});IY=class IY extends H0{visit($){return this.getVisitFn($.type).call(this,$)}visitNull($){let{["type"]:Z,["offset"]:Q=0,["length"]:X=0}=$;return new A0(Z,Q,X,X)}visitBool($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length>>3,["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitInt($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length,["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitFloat($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length,["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitUtf8($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.data),J=$0($.nullBitmap),W=oJ($.valueOffsets),{["length"]:Y=W.length-1,["nullCount"]:G=$.nullBitmap?-1:0}=$;return new A0(Z,Q,Y,G,[W,X,J])}visitUtf8View($){var Z;let{["type"]:Q,["offset"]:X=0}=$,J=S0(Q.ArrayType,$.views),W=$0($.nullBitmap),Y=($.variadicBuffers||[]).map((z)=>$0(z)),G=(Z=$.length)!==null&&Z!==void 0?Z:Math.trunc(J.length/OQ.ELEMENT_WIDTH),H=$.nullBitmap?-1:0;return new A0(Q,X,G,H,[void 0,J,W],[],void 0,Y)}visitLargeUtf8($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.data),J=$0($.nullBitmap),W=X4($.valueOffsets),{["length"]:Y=W.length-1,["nullCount"]:G=$.nullBitmap?-1:0}=$;return new A0(Z,Q,Y,G,[W,X,J])}visitBinary($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.data),J=$0($.nullBitmap),W=oJ($.valueOffsets),{["length"]:Y=W.length-1,["nullCount"]:G=$.nullBitmap?-1:0}=$;return new A0(Z,Q,Y,G,[W,X,J])}visitBinaryView($){var Z;let{["type"]:Q,["offset"]:X=0}=$,J=S0(Q.ArrayType,$.views),W=$0($.nullBitmap),Y=($.variadicBuffers||[]).map((z)=>$0(z)),G=(Z=$.length)!==null&&Z!==void 0?Z:Math.trunc(J.length/R0.ELEMENT_WIDTH),H=$.nullBitmap?-1:0;return new A0(Q,X,G,H,[void 0,J,W],[],void 0,Y)}visitLargeBinary($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.data),J=$0($.nullBitmap),W=X4($.valueOffsets),{["length"]:Y=W.length-1,["nullCount"]:G=$.nullBitmap?-1:0}=$;return new A0(Z,Q,Y,G,[W,X,J])}visitFixedSizeBinary($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length/RQ(Z),["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitDate($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length/RQ(Z),["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitTimestamp($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length/RQ(Z),["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitTime($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length/RQ(Z),["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitDecimal($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length/RQ(Z),["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitList($){let{["type"]:Z,["offset"]:Q=0,["child"]:X}=$,J=$0($.nullBitmap),W=oJ($.valueOffsets),{["length"]:Y=W.length-1,["nullCount"]:G=$.nullBitmap?-1:0}=$;return new A0(Z,Q,Y,G,[W,void 0,J],[X])}visitStruct($){let{["type"]:Z,["offset"]:Q=0,["children"]:X=[]}=$,J=$0($.nullBitmap),{length:W=X.reduce((G,{length:H})=>Math.max(G,H),0),nullCount:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,void 0,J],X)}visitUnion($){let{["type"]:Z,["offset"]:Q=0,["children"]:X=[]}=$,J=S0(Z.ArrayType,$.typeIds),{["length"]:W=J.length,["nullCount"]:Y=-1}=$;if(k.isSparseUnion(Z))return new A0(Z,Q,W,Y,[void 0,void 0,void 0,J],X);let G=oJ($.valueOffsets);return new A0(Z,Q,W,Y,[G,void 0,void 0,J],X)}visitDictionary($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.indices.ArrayType,$.data),{["dictionary"]:W=new r([new IY().visit({type:Z.dictionary})])}=$,{["length"]:Y=J.length,["nullCount"]:G=$.nullBitmap?-1:0}=$;return new A0(Z,Q,Y,G,[void 0,J,X],[],W)}visitInterval($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length/RQ(Z),["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitDuration($){let{["type"]:Z,["offset"]:Q=0}=$,X=$0($.nullBitmap),J=S0(Z.ArrayType,$.data),{["length"]:W=J.length,["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,J,X])}visitFixedSizeList($){let{["type"]:Z,["offset"]:Q=0,["child"]:X=new IY().visit({type:Z.valueType})}=$,J=$0($.nullBitmap),{["length"]:W=X.length/RQ(Z),["nullCount"]:Y=$.nullBitmap?-1:0}=$;return new A0(Z,Q,W,Y,[void 0,void 0,J],[X])}visitMap($){let{["type"]:Z,["offset"]:Q=0,["child"]:X=new IY().visit({type:Z.childType})}=$,J=$0($.nullBitmap),W=oJ($.valueOffsets),{["length"]:Y=W.length-1,["nullCount"]:G=$.nullBitmap?-1:0}=$;return new A0(Z,Q,Y,G,[W,void 0,J],[X])}};nO=new IY});function XP($){return $.some((Z)=>Z.nullable)}function g4($){return $.reduce((Z,Q)=>Z+Q.nullCount,0)}function x4($){return $.reduce((Z,Q,X)=>{return Z[X+1]=Z[X]+Q.length,Z},new Uint32Array($.length+1))}function v4($,Z,Q,X){let J=[];for(let W=-1,Y=$.length;++W=X)break;if(Q>=H+z)continue;if(H>=Q&&H+z<=X){J.push(G);continue}let q=Math.max(0,Q-H),P=Math.min(X-H,z);J.push(G.slice(q,P-q))}if(J.length===0)J.push($[0].slice(0,0));return J}function W2($,Z,Q,X){let J=0,W=0,Y=Z.length-1;do{if(J>=Y-1)return Q{T4=class T4{constructor($=0,Z){this.numChunks=$,this.getChunkIterator=Z,this.chunkIndex=0,this.chunkIterator=this.getChunkIterator(0)}next(){while(this.chunkIndex0?0:-1}function sO($,Z){let{nullBitmap:Q}=$;if(!Q||$.nullCount<=0)return-1;let X=0;for(let J of new YW(Q,$.offset+(Z||0),$.length,Q,_4)){if(!J)return X;++X}return-1}function U0($,Z,Q){if(Z===void 0)return-1;if(Z===null)switch($.typeId){case A.Union:break;case A.Dictionary:break;default:return sO($,Q)}let X=p$.getVisitFn($),J=PJ(Z);for(let W=(Q||0)-1,Y=$.length;++W{u$();R8();aJ();GW();WW();W0=class W0 extends H0{};W0.prototype.visitNull=oO;W0.prototype.visitBool=U0;W0.prototype.visitInt=U0;W0.prototype.visitInt8=U0;W0.prototype.visitInt16=U0;W0.prototype.visitInt32=U0;W0.prototype.visitInt64=U0;W0.prototype.visitUint8=U0;W0.prototype.visitUint16=U0;W0.prototype.visitUint32=U0;W0.prototype.visitUint64=U0;W0.prototype.visitFloat=U0;W0.prototype.visitFloat16=U0;W0.prototype.visitFloat32=U0;W0.prototype.visitFloat64=U0;W0.prototype.visitUtf8=U0;W0.prototype.visitLargeUtf8=U0;W0.prototype.visitUtf8View=U0;W0.prototype.visitBinary=U0;W0.prototype.visitLargeBinary=U0;W0.prototype.visitBinaryView=U0;W0.prototype.visitFixedSizeBinary=U0;W0.prototype.visitDate=U0;W0.prototype.visitDateDay=U0;W0.prototype.visitDateMillisecond=U0;W0.prototype.visitTimestamp=U0;W0.prototype.visitTimestampSecond=U0;W0.prototype.visitTimestampMillisecond=U0;W0.prototype.visitTimestampMicrosecond=U0;W0.prototype.visitTimestampNanosecond=U0;W0.prototype.visitTime=U0;W0.prototype.visitTimeSecond=U0;W0.prototype.visitTimeMillisecond=U0;W0.prototype.visitTimeMicrosecond=U0;W0.prototype.visitTimeNanosecond=U0;W0.prototype.visitDecimal=U0;W0.prototype.visitList=U0;W0.prototype.visitStruct=U0;W0.prototype.visitUnion=U0;W0.prototype.visitDenseUnion=JP;W0.prototype.visitSparseUnion=JP;W0.prototype.visitDictionary=U0;W0.prototype.visitInterval=U0;W0.prototype.visitIntervalDayTime=U0;W0.prototype.visitIntervalYearMonth=U0;W0.prototype.visitIntervalMonthDayNano=U0;W0.prototype.visitDuration=U0;W0.prototype.visitDurationSecond=U0;W0.prototype.visitDurationMillisecond=U0;W0.prototype.visitDurationMicrosecond=U0;W0.prototype.visitDurationNanosecond=U0;W0.prototype.visitFixedSizeList=U0;W0.prototype.visitMap=U0;Z6=new W0});function q0($){let{type:Z}=$;if($.nullCount===0&&$.stride===1&&(k.isInt(Z)&&Z.bitWidth!==64||k.isTime(Z)&&Z.bitWidth!==64||k.isFloat(Z)&&Z.precision!==J$.HALF))return new T4($.data.length,(X)=>{let J=$.data[X];return J.values.subarray(0,J.length)[Symbol.iterator]()});let Q=0;return new T4($.data.length,(X)=>{let W=$.data[X].length,Y=$.slice(Q,Q+W);return Q+=W,new WP(Y)})}var Y0,WP,HW;var f4=O(()=>{R8();u$();H$();b4();Y0=class Y0 extends H0{};WP=class WP{constructor($){this.vector=$,this.index=0}next(){if(this.indexiO(Z)));if(ArrayBuffer.isView($)){if($ instanceof DataView)$=new Uint8Array($.buffer);let Z={offset:0,length:$.length,nullCount:-1,data:$};if($ instanceof Int8Array)return new r([I(Object.assign(Object.assign({},Z),{type:new s8}))]);if($ instanceof Int16Array)return new r([I(Object.assign(Object.assign({},Z),{type:new i8}))]);if($ instanceof Int32Array)return new r([I(Object.assign(Object.assign({},Z),{type:new GZ}))]);if($ instanceof BigInt64Array)return new r([I(Object.assign(Object.assign({},Z),{type:new AZ}))]);if($ instanceof Uint8Array||$ instanceof Uint8ClampedArray)return new r([I(Object.assign(Object.assign({},Z),{type:new r8}))]);if($ instanceof Uint16Array)return new r([I(Object.assign(Object.assign({},Z),{type:new a8}))]);if($ instanceof Uint32Array)return new r([I(Object.assign(Object.assign({},Z),{type:new t8}))]);if($ instanceof BigUint64Array)return new r([I(Object.assign(Object.assign({},Z),{type:new VQ}))]);if($ instanceof Float32Array)return new r([I(Object.assign(Object.assign({},Z),{type:new e8}))]);if($ instanceof Float64Array)return new r([I(Object.assign(Object.assign({},Z),{type:new $Q}))]);throw Error("Unrecognized input")}}throw Error("Unrecognized input")}function iO($){return $ instanceof A0?[$]:$ instanceof r?$.data:Q6($).data}var YP,GP,KP,HP,r,u4;var ZQ=O(()=>{u$();WW();H$();$8();b4();aJ();EQ();m4();f4();H$();GP=Symbol.for("apache-arrow/Vector"),KP={},HP={};r=class r{static isVector($){return($===null||$===void 0?void 0:$[GP])===!0}constructor($){var Z,Q,X;let J=$[0]instanceof r?$.flatMap((Y)=>Y.data):$;if(J.length===0||J.some((Y)=>!(Y instanceof A0)))throw TypeError("Vector constructor expects an Array of Data instances.");let W=(Z=J[0])===null||Z===void 0?void 0:Z.type;switch(J.length){case 0:this._offsets=[0];break;case 1:{let{get:Y,set:G,indexOf:H}=KP[W.typeId],z=J[0];this.isValid=(q)=>_Y(z,q),this.get=(q)=>Y(z,q),this.set=(q,P)=>G(z,q,P),this.indexOf=(q)=>H(z,q),this._offsets=[0,z.length];break}default:Object.setPrototypeOf(this,HP[W.typeId]),this._offsets=x4(J);break}this.data=J,this.type=W,this.stride=RQ(W),this.numChildren=(X=(Q=W.children)===null||Q===void 0?void 0:Q.length)!==null&&X!==void 0?X:0,this.length=this._offsets.at(-1)}get byteLength(){return this.data.reduce(($,Z)=>$+Z.byteLength,0)}get nullable(){return XP(this.data)}get nullCount(){return g4(this.data)}get ArrayType(){return this.type.ArrayType}get[Symbol.toStringTag](){return`${this.VectorName}<${this.type[Symbol.toStringTag]}>`}get VectorName(){return`${A[this.type.typeId]}Vector`}isValid($){return!1}get($){return null}at($){return this.get(tJ($,this.length))}set($,Z){return}indexOf($,Z){return-1}includes($,Z){return this.indexOf($,Z)>-1}[Symbol.iterator](){return HW.visit(this)}concat(...$){return new r(this.data.concat($.flatMap((Z)=>Z.data).flat(Number.POSITIVE_INFINITY)))}slice($,Z){return new r(BY(this,$,Z,({data:Q,_offsets:X},J,W)=>v4(Q,X,J,W)))}toJSON(){return[...this]}toArray(){let{type:$,data:Z,length:Q,stride:X,ArrayType:J}=this;switch($.typeId){case A.Int:case A.Float:case A.Decimal:case A.Time:case A.Timestamp:switch(Z.length){case 0:return new J;case 1:return Z[0].values.subarray(0,Q*X);default:return Z.reduce((W,{values:Y,length:G})=>{return W.array.set(Y.subarray(0,G*X),W.offset),W.offset+=G*X,W},{array:new J(Q*X),offset:0}).array}}return[...this]}toString(){return`[${[...this].join(",")}]`}getChild($){var Z;return this.getChildAt((Z=this.type.children)===null||Z===void 0?void 0:Z.findIndex((Q)=>Q.name===$))}getChildAt($){if($>-1&&$Z[$]));return null}get isMemoized(){if(k.isDictionary(this.type))return this.data[0].dictionary.isMemoized;return!1}memoize(){if(k.isDictionary(this.type)){let $=new u4(this.data[0].dictionary),Z=this.data.map((Q)=>{let X=Q.clone();return X.dictionary=$,X});return new r(Z)}return new u4(this)}unmemoize(){if(k.isDictionary(this.type)&&this.isMemoized){let $=this.data[0].dictionary.unmemoize(),Z=this.data.map((Q)=>{let X=Q.clone();return X.dictionary=$,X});return new r(Z)}return this}};YP=Symbol.toStringTag;r[YP]=(($)=>{$.type=k.prototype,$.data=[],$.length=0,$.stride=1,$.numChildren=0,$._offsets=new Uint32Array([0]),$[Symbol.isConcatSpreadable]=!0,$[GP]=!0;let Z=Object.keys(A).map((Q)=>A[Q]).filter((Q)=>typeof Q==="number"&&Q!==A.NONE);for(let Q of Z){let X=p$.getVisitFnByTypeId(Q),J=DZ.getVisitFnByTypeId(Q),W=Z6.getVisitFnByTypeId(Q);KP[Q]={get:X,set:J,indexOf:W},HP[Q]=Object.create($,{["isValid"]:{value:KW(_Y)},["get"]:{value:KW(p$.getVisitFnByTypeId(Q))},["set"]:{value:y4(DZ.getVisitFnByTypeId(Q))},["indexOf"]:{value:h4(Z6.getVisitFnByTypeId(Q))}})}return"Vector"})(r.prototype);Object.defineProperty(r,Symbol.hasInstance,{value:function(Z){return Function.prototype[Symbol.hasInstance].call(this,Z)||this===r&&r.isVector(Z)}});u4=class u4 extends r{constructor($){super($.data);let Z=this.get,Q=this.set,X=this.slice,J=Array(this.length);Object.defineProperty(this,"get",{value(W){let Y=J[W];if(Y!==void 0)return Y;let G=Z.call(this,W);return J[W]=G,G}}),Object.defineProperty(this,"set",{value(W,Y){Q.call(this,W,Y),J[W]=Y}}),Object.defineProperty(this,"slice",{value:(W,Y)=>new u4(X.call(this,W,Y))}),Object.defineProperty(this,"isMemoized",{value:!0}),Object.defineProperty(this,"unmemoize",{value:()=>new r(this.data)}),Object.defineProperty(this,"memoize",{value:()=>this})}}});function zP($){if(!$||$.length<=0)return function(J){return!0};let Z="",Q=$.filter((X)=>X===X);if(Q.length>0)Z=` - switch (x) {${Q.map((X)=>` - case ${rO(X)}:`).join("")} +var Yb=Object.defineProperty;var Kb=(t)=>t;function Gb(t,e){this[t]=Kb.bind(null,e)}var mr=(t,e)=>{for(var n in e)Yb(t,n,{get:e[n],enumerable:!0,configurable:!0,set:Gb.bind(e,n)})};var b=(t,e,n)=>()=>{if(t)try{e=t(t=0)}catch(r){n=[r]}if(n)throw n[0];return e};function Jp(t,e){var n={};for(var r in t)if(Object.prototype.hasOwnProperty.call(t,r)&&e.indexOf(r)<0)n[r]=t[r];if(t!=null&&typeof Object.getOwnPropertySymbols==="function"){for(var a=0,r=Object.getOwnPropertySymbols(t);a=t.length)t=void 0;return{value:t&&t[r++],done:!t}}};throw TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")}function st(t){return this instanceof st?(this.v=t,this):new st(t)}function ln(t,e,n){if(!Symbol.asyncIterator)throw TypeError("Symbol.asyncIterator is not defined.");var r=n.apply(t,e||[]),a,o=[];return a=Object.create((typeof AsyncIterator==="function"?AsyncIterator:Object).prototype),c("next"),c("throw"),c("return",s),a[Symbol.asyncIterator]=function(){return this},a;function s(w){return function(I){return Promise.resolve(I).then(w,_)}}function c(w,I){if(r[w]){if(a[w]=function(x){return new Promise(function(C,D){o.push([w,x,C,D])>1||l(w,x)})},I)a[w]=I(a[w])}}function l(w,I){try{f(r[w](I))}catch(x){g(o[0][3],x)}}function f(w){w.value instanceof st?Promise.resolve(w.value.v).then(p,_):g(o[0][2],w)}function p(w){l("next",w)}function _(w){l("throw",w)}function g(w,I){if(w(I),o.shift(),o.length)l(o[0][0],o[0][1])}}function _a(t){var e,n;return e={},r("next"),r("throw",function(a){throw a}),r("return"),e[Symbol.iterator]=function(){return this},e;function r(a,o){e[a]=t[a]?function(s){return(n=!n)?{value:st(t[a](s)),done:!1}:o?o(s):s}:o}}function Kn(t){if(!Symbol.asyncIterator)throw TypeError("Symbol.asyncIterator is not defined.");var e=t[Symbol.asyncIterator],n;return e?e.call(t):(t=typeof Xp==="function"?Xp(t):t[Symbol.iterator](),n={},r("next"),r("throw"),r("return"),n[Symbol.asyncIterator]=function(){return this},n);function r(o){n[o]=t[o]&&function(s){return new Promise(function(c,l){s=t[o](s),a(c,l,s.done,s.value)})}}function a(o,s,c,l){Promise.resolve(l).then(function(f){o({value:f,done:c})},s)}}var un=()=>{};var Zp,ga,Wb,fn=(t)=>Wb.encode(t);var Pr=b(()=>{Zp=new TextDecoder("utf-8"),ga=Zp.decode.bind(Zp),Wb=new TextEncoder});var Qb=(t)=>typeof t==="number",th=(t)=>typeof t==="boolean",ae=(t)=>typeof t==="function",Ve=(t)=>t!=null&&Object(t)===t,Gn=(t)=>Ve(t)&&ae(t.then),Wn=(t)=>Ve(t)&&ae(t[Symbol.iterator]),En=(t)=>Ve(t)&&ae(t[Symbol.asyncIterator]),js=(t)=>Ve(t)&&Ve(t.schema),$s=(t)=>Ve(t)&&("done"in t)&&("value"in t),Hs=(t)=>Ve(t)&&ae(t.stat)&&Qb(t.fd),Ys=(t)=>Ve(t)&&go(t.body),Ks=(t)=>("_getDOMStream"in t)&&("_getNodeStream"in t),eh=(t)=>Ve(t)&&ae(t.abort)&&ae(t.getWriter)&&!Ks(t),go=(t)=>Ve(t)&&ae(t.cancel)&&ae(t.getReader)&&!Ks(t),nh=(t)=>Ve(t)&&ae(t.end)&&ae(t.write)&&th(t.writable)&&!Ks(t),Gs=(t)=>Ve(t)&&ae(t.read)&&ae(t.pipe)&&th(t.readable)&&!Ks(t),rh=(t)=>Ve(t)&&ae(t.clear)&&ae(t.bytes)&&ae(t.position)&&ae(t.setPosition)&&ae(t.capacity)&&ae(t.getBufferIdentifier)&&ae(t.createLong);var ku={};mr(ku,{compareArrayLike:()=>Pu,joinUint8Arrays:()=>dn,memcpy:()=>bo,rebaseValueOffsets:()=>Qs,toArrayBufferView:()=>ut,toArrayBufferViewAsyncIterator:()=>Qn,toArrayBufferViewIterator:()=>gr,toBigInt64Array:()=>Ws,toBigUint64Array:()=>nw,toFloat32Array:()=>rw,toFloat32ArrayAsyncIterator:()=>bw,toFloat32ArrayIterator:()=>fw,toFloat64Array:()=>iw,toFloat64ArrayAsyncIterator:()=>ww,toFloat64ArrayIterator:()=>dw,toInt16Array:()=>Zb,toInt16ArrayAsyncIterator:()=>mw,toInt16ArrayIterator:()=>sw,toInt32Array:()=>zi,toInt32ArrayAsyncIterator:()=>yw,toInt32ArrayIterator:()=>cw,toInt8Array:()=>Jb,toInt8ArrayAsyncIterator:()=>hw,toInt8ArrayIterator:()=>ow,toUint16Array:()=>tw,toUint16ArrayAsyncIterator:()=>_w,toUint16ArrayIterator:()=>lw,toUint32Array:()=>ew,toUint32ArrayAsyncIterator:()=>gw,toUint32ArrayIterator:()=>uw,toUint8Array:()=>L,toUint8ArrayAsyncIterator:()=>Lu,toUint8ArrayIterator:()=>Mu,toUint8ClampedArray:()=>aw,toUint8ClampedArrayAsyncIterator:()=>xw,toUint8ClampedArrayIterator:()=>pw});function Xb(t){let e=t[0]?[t[0]]:[],n,r,a,o;for(let s,c,l=0,f=0,p=t.length;++lp+_.byteLength,0),a,o,s,c=0,l=-1,f=Math.min(e||Number.POSITIVE_INFINITY,r);for(let p=n.length;++l0)do if(t[n]!==e[n])return!1;while(++nut(Int8Array,t),Zb=(t)=>ut(Int16Array,t),zi=(t)=>ut(Int32Array,t),Ws=(t)=>ut(BigInt64Array,t),L=(t)=>ut(Uint8Array,t),tw=(t)=>ut(Uint16Array,t),ew=(t)=>ut(Uint32Array,t),nw=(t)=>ut(BigUint64Array,t),rw=(t)=>ut(Float32Array,t),iw=(t)=>ut(Float64Array,t),aw=(t)=>ut(Uint8ClampedArray,t),Fu=(t)=>(t.next(),t),ow=(t)=>gr(Int8Array,t),sw=(t)=>gr(Int16Array,t),cw=(t)=>gr(Int32Array,t),Mu=(t)=>gr(Uint8Array,t),lw=(t)=>gr(Uint16Array,t),uw=(t)=>gr(Uint32Array,t),fw=(t)=>gr(Float32Array,t),dw=(t)=>gr(Float64Array,t),pw=(t)=>gr(Uint8ClampedArray,t),hw=(t)=>Qn(Int8Array,t),mw=(t)=>Qn(Int16Array,t),yw=(t)=>Qn(Int32Array,t),Lu=(t)=>Qn(Uint8Array,t),_w=(t)=>Qn(Uint16Array,t),gw=(t)=>Qn(Uint32Array,t),bw=(t)=>Qn(Float32Array,t),ww=(t)=>Qn(Float64Array,t),xw=(t)=>Qn(Uint8ClampedArray,t);var oe=b(()=>{un();Pr();Uu=typeof SharedArrayBuffer<"u"?SharedArrayBuffer:ArrayBuffer});function*Sw(t){let e,n=!1,r=[],a,o,s,c=0;function l(){if(o==="peek")return dn(r,s)[0];return[a,r,c]=dn(r,s),a}({cmd:o,size:s}=(yield(()=>null)())||{cmd:"read",size:0});let f=Mu(t)[Symbol.iterator]();try{do{if({done:e,value:a}=Number.isNaN(s-c)?f.next():f.next(s-c),!e&&a.byteLength>0)r.push(a),c+=a.byteLength;if(e||s<=c)do({cmd:o,size:s}=yield l());while(snull)()))||{cmd:"read",size:0});let p=Lu(t)[Symbol.asyncIterator]();try{do{if({done:n,value:o}=Number.isNaN(c-l)?yield st(p.next()):yield st(p.next(c-l)),!n&&o.byteLength>0)a.push(o),l+=o.byteLength;if(n||c<=l)do({cmd:s,size:c}=yield yield st(f()));while(cnull)()))||{cmd:"read",size:0});let p=new ih(t);try{do{if({done:n,value:o}=Number.isNaN(c-l)?yield st(p.read()):yield st(p.read(c-l)),!n&&o.byteLength>0)a.push(L(o)),l+=o.byteLength;if(n||c<=l)do({cmd:s,size:c}=yield yield st(f()));while(c{})}get closed(){return this.reader?this.reader.closed.catch(()=>{}):Promise.resolve()}releaseLock(){if(this.reader)this.reader.releaseLock();this.reader=null}cancel(t){return R(this,void 0,void 0,function*(){let{reader:e,source:n}=this;e&&(yield e.cancel(t).catch(()=>{})),n&&(n.locked&&this.releaseLock())})}read(t){return R(this,void 0,void 0,function*(){if(t===0)return{done:this.reader==null,value:new Uint8Array(0)};let e=yield this.reader.read();return!e.done&&(e.value=L(e)),e})}}function Tw(t){return ln(this,arguments,function*(){let n=[],r="error",a=!1,o=null,s,c,l=0,f=[],p;function _(){if(s==="peek")return dn(f,c)[0];return[p,f,l]=dn(f,c),p}if({cmd:s,size:c}=(yield yield st((()=>null)()))||{cmd:"read",size:0},t.isTTY)return yield yield st(new Uint8Array(0)),yield st(null);try{n[0]=Vu(t,"end"),n[1]=Vu(t,"error");do{if(n[2]=Vu(t,"readable"),[r,o]=yield st(Promise.race(n.map((w)=>w[2]))),r==="error")break;if(!(a=r==="end")){if(!Number.isFinite(c-l))p=L(t.read());else if(p=L(t.read(c-l)),p.byteLength0)f.push(p),l+=p.byteLength}if(a||c<=l)do({cmd:s,size:c}=yield yield st(_()));while(c{for(let[D,Y]of w)t.off(D,Y);try{let D=t.destroy;D&&D.call(t,I),I=void 0}catch(D){I=D||I}finally{I!=null?C(I):x()}})}})}var Ae,Xs=(t)=>(t.next(),t),Vu=(t,e)=>{let n=(a)=>r([e,a]),r;return[e,n,new Promise((a)=>(r=a)&&t.once(e,n))]};var wo=b(()=>{un();oe();Ae={fromIterable(t){return Xs(Sw(t))},fromAsyncIterable(t){return Xs(Aw(t))},fromDOMStream(t){return Xs(Iw(t))},fromNodeStream(t){return Xs(Tw(t))},toDOMStream(t,e){throw Error('"toDOMStream" not available in this environment')},toNodeStream(t,e){throw Error('"toNodeStream" not available in this environment')}}});var Tt;var Js=b(()=>{(function(t){t[t.V1=0]="V1",t[t.V2=1]="V2",t[t.V3=2]="V3",t[t.V4=3]="V4",t[t.V5=4]="V5"})(Tt||(Tt={}))});var yt;var qu=b(()=>{(function(t){t[t.Sparse=0]="Sparse",t[t.Dense=1]="Dense"})(yt||(yt={}))});var Rt;var zu=b(()=>{(function(t){t[t.HALF=0]="HALF",t[t.SINGLE=1]="SINGLE",t[t.DOUBLE=2]="DOUBLE"})(Rt||(Rt={}))});var vt;var ju=b(()=>{(function(t){t[t.DAY=0]="DAY",t[t.MILLISECOND=1]="MILLISECOND"})(vt||(vt={}))});var E;var xo=b(()=>{(function(t){t[t.SECOND=0]="SECOND",t[t.MILLISECOND=1]="MILLISECOND",t[t.MICROSECOND=2]="MICROSECOND",t[t.NANOSECOND=3]="NANOSECOND"})(E||(E={}))});var It;var $u=b(()=>{(function(t){t[t.YEAR_MONTH=0]="YEAR_MONTH",t[t.DAY_TIME=1]="DAY_TIME",t[t.MONTH_DAY_NANO=2]="MONTH_DAY_NANO"})(It||(It={}))});var So=2,pn=4,Dn=4,U=4;var br,Zs,tc,ba;var Hu=b(()=>{br=new Int32Array(2),Zs=new Float32Array(br.buffer),tc=new Float64Array(br.buffer),ba=new Uint16Array(new Uint8Array([1,0]).buffer)[0]===1});var Ao;var Yu=b(()=>{(function(t){t[t.UTF8_BYTES=1]="UTF8_BYTES",t[t.UTF16_STRING=2]="UTF16_STRING"})(Ao||(Ao={}))});class Ie{constructor(t){this.bytes_=t,this.position_=0,this.text_decoder_=new TextDecoder}static allocate(t){return new Ie(new Uint8Array(t))}clear(){this.position_=0}bytes(){return this.bytes_}position(){return this.position_}setPosition(t){this.position_=t}capacity(){return this.bytes_.length}readInt8(t){return this.readUint8(t)<<24>>24}readUint8(t){return this.bytes_[t]}readInt16(t){return this.readUint16(t)<<16>>16}readUint16(t){return this.bytes_[t]|this.bytes_[t+1]<<8}readInt32(t){return this.bytes_[t]|this.bytes_[t+1]<<8|this.bytes_[t+2]<<16|this.bytes_[t+3]<<24}readUint32(t){return this.readInt32(t)>>>0}readInt64(t){return BigInt.asIntN(64,BigInt(this.readUint32(t))+(BigInt(this.readUint32(t+4))<>8}writeUint16(t,e){this.bytes_[t]=e,this.bytes_[t+1]=e>>8}writeInt32(t,e){this.bytes_[t]=e,this.bytes_[t+1]=e>>8,this.bytes_[t+2]=e>>16,this.bytes_[t+3]=e>>24}writeUint32(t,e){this.bytes_[t]=e,this.bytes_[t+1]=e>>8,this.bytes_[t+2]=e>>16,this.bytes_[t+3]=e>>24}writeInt64(t,e){this.writeInt32(t,Number(BigInt.asIntN(32,e))),this.writeInt32(t+4,Number(BigInt.asIntN(32,e>>BigInt(32))))}writeUint64(t,e){this.writeUint32(t,Number(BigInt.asUintN(32,e))),this.writeUint32(t+4,Number(BigInt.asUintN(32,e>>BigInt(32))))}writeFloat32(t,e){Zs[0]=e,this.writeInt32(t,br[0])}writeFloat64(t,e){tc[0]=e,this.writeInt32(t,br[ba?0:1]),this.writeInt32(t+4,br[ba?1:0])}getBufferIdentifier(){if(this.bytes_.length{Yu();Hu()});class ji{constructor(t){this.minalign=1,this.vtable=null,this.vtable_in_use=0,this.isNested=!1,this.object_start=0,this.vtables=[],this.vector_num_elems=0,this.force_defaults=!1,this.string_maps=null,this.text_encoder=new TextEncoder;let e;if(!t)e=1024;else e=t;this.bb=Ie.allocate(e),this.space=e}clear(){this.bb.clear(),this.space=this.bb.capacity(),this.minalign=1,this.vtable=null,this.vtable_in_use=0,this.isNested=!1,this.object_start=0,this.vtables=[],this.vector_num_elems=0,this.force_defaults=!1,this.string_maps=null}forceDefaults(t){this.force_defaults=t}dataBuffer(){return this.bb}asUint8Array(){return this.bb.bytes().subarray(this.bb.position(),this.bb.position()+this.offset())}prep(t,e){if(t>this.minalign)this.minalign=t;let n=~(this.bb.capacity()-this.space+e)+1&t-1;while(this.space=0&&this.vtable[e]==0;e--);let n=e+1;for(;e>=0;e--)this.addInt16(this.vtable[e]!=0?t-this.vtable[e]:0);let r=2;this.addInt16(t-this.object_start);let a=(n+r)*So;this.addInt16(a);let o=0,s=this.space;t:for(e=0;e=0;o--)this.writeInt8(a.charCodeAt(o))}if(this.prep(this.minalign,pn+r),this.addOffset(t),r)this.addInt32(this.bb.capacity()-this.space);this.bb.setPosition(this.space)}finishSizePrefixed(t,e){this.finish(t,e,!0)}requiredField(t,e){let n=this.bb.capacity()-t,r=n-this.bb.readInt32(n);if(!(e{Ku()});var ht=b(()=>{Hu();ah();Ku();Yu()});var $i;var Gu=b(()=>{(function(t){t[t.BUFFER=0]="BUFFER"})($i||($i={}))});var qe;var Io=b(()=>{(function(t){t[t.LZ4_FRAME=0]="LZ4_FRAME",t[t.ZSTD=1]="ZSTD"})(qe||(qe={}))});class Xe{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsBodyCompression(t,e){return(e||new Xe).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsBodyCompression(t,e){return t.setPosition(t.position()+U),(e||new Xe).__init(t.readInt32(t.position())+t.position(),t)}codec(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt8(this.bb_pos+t):qe.LZ4_FRAME}method(){let t=this.bb.__offset(this.bb_pos,6);return t?this.bb.readInt8(this.bb_pos+t):$i.BUFFER}static startBodyCompression(t){t.startObject(2)}static addCodec(t,e){t.addFieldInt8(0,e,qe.LZ4_FRAME)}static addMethod(t,e){t.addFieldInt8(1,e,$i.BUFFER)}static endBodyCompression(t){return t.endObject()}static createBodyCompression(t,e,n){return Xe.startBodyCompression(t),Xe.addCodec(t,e),Xe.addMethod(t,n),Xe.endBodyCompression(t)}}var Wu=b(()=>{ht();Gu();Io()});class To{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}offset(){return this.bb.readInt64(this.bb_pos)}length(){return this.bb.readInt64(this.bb_pos+8)}static sizeOf(){return 16}static createBuffer(t,e,n){return t.prep(8,16),t.writeInt64(BigInt(n!==null&&n!==void 0?n:0)),t.writeInt64(BigInt(e!==null&&e!==void 0?e:0)),t.offset()}}class vo{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}length(){return this.bb.readInt64(this.bb_pos)}nullCount(){return this.bb.readInt64(this.bb_pos+8)}static sizeOf(){return 16}static createFieldNode(t,e,n){return t.prep(8,16),t.writeInt64(BigInt(n!==null&&n!==void 0?n:0)),t.writeInt64(BigInt(e!==null&&e!==void 0?e:0)),t.offset()}}class Te{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsRecordBatch(t,e){return(e||new Te).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsRecordBatch(t,e){return t.setPosition(t.position()+U),(e||new Te).__init(t.readInt32(t.position())+t.position(),t)}length(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt64(this.bb_pos+t):BigInt("0")}nodes(t,e){let n=this.bb.__offset(this.bb_pos,6);return n?(e||new vo).__init(this.bb.__vector(this.bb_pos+n)+t*16,this.bb):null}nodesLength(){let t=this.bb.__offset(this.bb_pos,6);return t?this.bb.__vector_len(this.bb_pos+t):0}buffers(t,e){let n=this.bb.__offset(this.bb_pos,8);return n?(e||new To).__init(this.bb.__vector(this.bb_pos+n)+t*16,this.bb):null}buffersLength(){let t=this.bb.__offset(this.bb_pos,8);return t?this.bb.__vector_len(this.bb_pos+t):0}compression(t){let e=this.bb.__offset(this.bb_pos,10);return e?(t||new Xe).__init(this.bb.__indirect(this.bb_pos+e),this.bb):null}variadicBufferCounts(t){let e=this.bb.__offset(this.bb_pos,12);return e?this.bb.readInt64(this.bb.__vector(this.bb_pos+e)+t*8):BigInt(0)}variadicBufferCountsLength(){let t=this.bb.__offset(this.bb_pos,12);return t?this.bb.__vector_len(this.bb_pos+t):0}static startRecordBatch(t){t.startObject(5)}static addLength(t,e){t.addFieldInt64(0,e,BigInt("0"))}static addNodes(t,e){t.addFieldOffset(1,e,0)}static startNodesVector(t,e){t.startVector(16,e,8)}static addBuffers(t,e){t.addFieldOffset(2,e,0)}static startBuffersVector(t,e){t.startVector(16,e,8)}static addCompression(t,e){t.addFieldOffset(3,e,0)}static addVariadicBufferCounts(t,e){t.addFieldOffset(4,e,0)}static createVariadicBufferCountsVector(t,e){t.startVector(8,e.length,8);for(let n=e.length-1;n>=0;n--)t.addInt64(e[n]);return t.endVector()}static startVariadicBufferCountsVector(t,e){t.startVector(8,e,8)}static endRecordBatch(t){return t.endObject()}}var Qu=b(()=>{ht();Wu()});class wr{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsDictionaryBatch(t,e){return(e||new wr).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsDictionaryBatch(t,e){return t.setPosition(t.position()+U),(e||new wr).__init(t.readInt32(t.position())+t.position(),t)}id(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt64(this.bb_pos+t):BigInt("0")}data(t){let e=this.bb.__offset(this.bb_pos,6);return e?(t||new Te).__init(this.bb.__indirect(this.bb_pos+e),this.bb):null}isDelta(){let t=this.bb.__offset(this.bb_pos,8);return t?!!this.bb.readInt8(this.bb_pos+t):!1}static startDictionaryBatch(t){t.startObject(3)}static addId(t,e){t.addFieldInt64(0,e,BigInt("0"))}static addData(t,e){t.addFieldOffset(1,e,0)}static addIsDelta(t,e){t.addFieldInt8(2,+e,0)}static endDictionaryBatch(t){return t.endObject()}}var oh=b(()=>{ht();Qu()});var ci;var Xu=b(()=>{(function(t){t[t.Little=0]="Little",t[t.Big=1]="Big"})(ci||(ci={}))});var Eo;var sh=b(()=>{(function(t){t[t.DenseArray=0]="DenseArray"})(Eo||(Eo={}))});class Ue{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsInt(t,e){return(e||new Ue).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsInt(t,e){return t.setPosition(t.position()+U),(e||new Ue).__init(t.readInt32(t.position())+t.position(),t)}bitWidth(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt32(this.bb_pos+t):0}isSigned(){let t=this.bb.__offset(this.bb_pos,6);return t?!!this.bb.readInt8(this.bb_pos+t):!1}static startInt(t){t.startObject(2)}static addBitWidth(t,e){t.addFieldInt32(0,e,0)}static addIsSigned(t,e){t.addFieldInt8(1,+e,0)}static endInt(t){return t.endObject()}static createInt(t,e,n){return Ue.startInt(t),Ue.addBitWidth(t,e),Ue.addIsSigned(t,n),Ue.endInt(t)}}var ec=b(()=>{ht()});class Xn{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsDictionaryEncoding(t,e){return(e||new Xn).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsDictionaryEncoding(t,e){return t.setPosition(t.position()+U),(e||new Xn).__init(t.readInt32(t.position())+t.position(),t)}id(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt64(this.bb_pos+t):BigInt("0")}indexType(t){let e=this.bb.__offset(this.bb_pos,6);return e?(t||new Ue).__init(this.bb.__indirect(this.bb_pos+e),this.bb):null}isOrdered(){let t=this.bb.__offset(this.bb_pos,8);return t?!!this.bb.readInt8(this.bb_pos+t):!1}dictionaryKind(){let t=this.bb.__offset(this.bb_pos,10);return t?this.bb.readInt16(this.bb_pos+t):Eo.DenseArray}static startDictionaryEncoding(t){t.startObject(4)}static addId(t,e){t.addFieldInt64(0,e,BigInt("0"))}static addIndexType(t,e){t.addFieldOffset(1,e,0)}static addIsOrdered(t,e){t.addFieldInt8(2,+e,0)}static addDictionaryKind(t,e){t.addFieldInt16(3,e,Eo.DenseArray)}static endDictionaryEncoding(t){return t.endObject()}}var Ju=b(()=>{ht();sh();ec()});class Nt{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsKeyValue(t,e){return(e||new Nt).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsKeyValue(t,e){return t.setPosition(t.position()+U),(e||new Nt).__init(t.readInt32(t.position())+t.position(),t)}key(t){let e=this.bb.__offset(this.bb_pos,4);return e?this.bb.__string(this.bb_pos+e,t):null}value(t){let e=this.bb.__offset(this.bb_pos,6);return e?this.bb.__string(this.bb_pos+e,t):null}static startKeyValue(t){t.startObject(2)}static addKey(t,e){t.addFieldOffset(0,e,0)}static addValue(t,e){t.addFieldOffset(1,e,0)}static endKeyValue(t){return t.endObject()}static createKeyValue(t,e,n){return Nt.startKeyValue(t),Nt.addKey(t,e),Nt.addValue(t,n),Nt.endKeyValue(t)}}var wa=b(()=>{ht()});class li{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsBinary(t,e){return(e||new li).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsBinary(t,e){return t.setPosition(t.position()+U),(e||new li).__init(t.readInt32(t.position())+t.position(),t)}static startBinary(t){t.startObject(0)}static endBinary(t){return t.endObject()}static createBinary(t){return li.startBinary(t),li.endBinary(t)}}var ch=b(()=>{ht()});class ui{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsBinaryView(t,e){return(e||new ui).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsBinaryView(t,e){return t.setPosition(t.position()+U),(e||new ui).__init(t.readInt32(t.position())+t.position(),t)}static startBinaryView(t){t.startObject(0)}static endBinaryView(t){return t.endObject()}static createBinaryView(t){return ui.startBinaryView(t),ui.endBinaryView(t)}}var lh=b(()=>{ht()});class fi{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsBool(t,e){return(e||new fi).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsBool(t,e){return t.setPosition(t.position()+U),(e||new fi).__init(t.readInt32(t.position())+t.position(),t)}static startBool(t){t.startObject(0)}static endBool(t){return t.endObject()}static createBool(t){return fi.startBool(t),fi.endBool(t)}}var uh=b(()=>{ht()});class Bn{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsDate(t,e){return(e||new Bn).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsDate(t,e){return t.setPosition(t.position()+U),(e||new Bn).__init(t.readInt32(t.position())+t.position(),t)}unit(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):vt.MILLISECOND}static startDate(t){t.startObject(1)}static addUnit(t,e){t.addFieldInt16(0,e,vt.MILLISECOND)}static endDate(t){return t.endObject()}static createDate(t,e){return Bn.startDate(t),Bn.addUnit(t,e),Bn.endDate(t)}}var Zu=b(()=>{ht();ju()});class Me{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsDecimal(t,e){return(e||new Me).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsDecimal(t,e){return t.setPosition(t.position()+U),(e||new Me).__init(t.readInt32(t.position())+t.position(),t)}precision(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt32(this.bb_pos+t):0}scale(){let t=this.bb.__offset(this.bb_pos,6);return t?this.bb.readInt32(this.bb_pos+t):0}bitWidth(){let t=this.bb.__offset(this.bb_pos,8);return t?this.bb.readInt32(this.bb_pos+t):128}static startDecimal(t){t.startObject(3)}static addPrecision(t,e){t.addFieldInt32(0,e,0)}static addScale(t,e){t.addFieldInt32(1,e,0)}static addBitWidth(t,e){t.addFieldInt32(2,e,128)}static endDecimal(t){return t.endObject()}static createDecimal(t,e,n,r){return Me.startDecimal(t),Me.addPrecision(t,e),Me.addScale(t,n),Me.addBitWidth(t,r),Me.endDecimal(t)}}var tf=b(()=>{ht()});class Cn{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsDuration(t,e){return(e||new Cn).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsDuration(t,e){return t.setPosition(t.position()+U),(e||new Cn).__init(t.readInt32(t.position())+t.position(),t)}unit(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):E.MILLISECOND}static startDuration(t){t.startObject(1)}static addUnit(t,e){t.addFieldInt16(0,e,E.MILLISECOND)}static endDuration(t){return t.endObject()}static createDuration(t,e){return Cn.startDuration(t),Cn.addUnit(t,e),Cn.endDuration(t)}}var ef=b(()=>{ht();xo()});class Rn{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsFixedSizeBinary(t,e){return(e||new Rn).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsFixedSizeBinary(t,e){return t.setPosition(t.position()+U),(e||new Rn).__init(t.readInt32(t.position())+t.position(),t)}byteWidth(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt32(this.bb_pos+t):0}static startFixedSizeBinary(t){t.startObject(1)}static addByteWidth(t,e){t.addFieldInt32(0,e,0)}static endFixedSizeBinary(t){return t.endObject()}static createFixedSizeBinary(t,e){return Rn.startFixedSizeBinary(t),Rn.addByteWidth(t,e),Rn.endFixedSizeBinary(t)}}var nf=b(()=>{ht()});class On{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsFixedSizeList(t,e){return(e||new On).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsFixedSizeList(t,e){return t.setPosition(t.position()+U),(e||new On).__init(t.readInt32(t.position())+t.position(),t)}listSize(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt32(this.bb_pos+t):0}static startFixedSizeList(t){t.startObject(1)}static addListSize(t,e){t.addFieldInt32(0,e,0)}static endFixedSizeList(t){return t.endObject()}static createFixedSizeList(t,e){return On.startFixedSizeList(t),On.addListSize(t,e),On.endFixedSizeList(t)}}var rf=b(()=>{ht()});class Nn{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsFloatingPoint(t,e){return(e||new Nn).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsFloatingPoint(t,e){return t.setPosition(t.position()+U),(e||new Nn).__init(t.readInt32(t.position())+t.position(),t)}precision(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):Rt.HALF}static startFloatingPoint(t){t.startObject(1)}static addPrecision(t,e){t.addFieldInt16(0,e,Rt.HALF)}static endFloatingPoint(t){return t.endObject()}static createFloatingPoint(t,e){return Nn.startFloatingPoint(t),Nn.addPrecision(t,e),Nn.endFloatingPoint(t)}}var af=b(()=>{ht();zu()});class Fn{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsInterval(t,e){return(e||new Fn).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsInterval(t,e){return t.setPosition(t.position()+U),(e||new Fn).__init(t.readInt32(t.position())+t.position(),t)}unit(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):It.YEAR_MONTH}static startInterval(t){t.startObject(1)}static addUnit(t,e){t.addFieldInt16(0,e,It.YEAR_MONTH)}static endInterval(t){return t.endObject()}static createInterval(t,e){return Fn.startInterval(t),Fn.addUnit(t,e),Fn.endInterval(t)}}var of=b(()=>{ht();$u()});class di{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsLargeBinary(t,e){return(e||new di).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsLargeBinary(t,e){return t.setPosition(t.position()+U),(e||new di).__init(t.readInt32(t.position())+t.position(),t)}static startLargeBinary(t){t.startObject(0)}static endLargeBinary(t){return t.endObject()}static createLargeBinary(t){return di.startLargeBinary(t),di.endLargeBinary(t)}}var fh=b(()=>{ht()});class pi{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsLargeUtf8(t,e){return(e||new pi).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsLargeUtf8(t,e){return t.setPosition(t.position()+U),(e||new pi).__init(t.readInt32(t.position())+t.position(),t)}static startLargeUtf8(t){t.startObject(0)}static endLargeUtf8(t){return t.endObject()}static createLargeUtf8(t){return pi.startLargeUtf8(t),pi.endLargeUtf8(t)}}var dh=b(()=>{ht()});class hi{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsList(t,e){return(e||new hi).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsList(t,e){return t.setPosition(t.position()+U),(e||new hi).__init(t.readInt32(t.position())+t.position(),t)}static startList(t){t.startObject(0)}static endList(t){return t.endObject()}static createList(t){return hi.startList(t),hi.endList(t)}}var ph=b(()=>{ht()});class Un{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsMap(t,e){return(e||new Un).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsMap(t,e){return t.setPosition(t.position()+U),(e||new Un).__init(t.readInt32(t.position())+t.position(),t)}keysSorted(){let t=this.bb.__offset(this.bb_pos,4);return t?!!this.bb.readInt8(this.bb_pos+t):!1}static startMap(t){t.startObject(1)}static addKeysSorted(t,e){t.addFieldInt8(0,+e,0)}static endMap(t){return t.endObject()}static createMap(t,e){return Un.startMap(t),Un.addKeysSorted(t,e),Un.endMap(t)}}var sf=b(()=>{ht()});class mi{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsNull(t,e){return(e||new mi).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsNull(t,e){return t.setPosition(t.position()+U),(e||new mi).__init(t.readInt32(t.position())+t.position(),t)}static startNull(t){t.startObject(0)}static endNull(t){return t.endObject()}static createNull(t){return mi.startNull(t),mi.endNull(t)}}var hh=b(()=>{ht()});class yi{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsStruct_(t,e){return(e||new yi).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsStruct_(t,e){return t.setPosition(t.position()+U),(e||new yi).__init(t.readInt32(t.position())+t.position(),t)}static startStruct_(t){t.startObject(0)}static endStruct_(t){return t.endObject()}static createStruct_(t){return yi.startStruct_(t),yi.endStruct_(t)}}var mh=b(()=>{ht()});class Je{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsTime(t,e){return(e||new Je).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsTime(t,e){return t.setPosition(t.position()+U),(e||new Je).__init(t.readInt32(t.position())+t.position(),t)}unit(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):E.MILLISECOND}bitWidth(){let t=this.bb.__offset(this.bb_pos,6);return t?this.bb.readInt32(this.bb_pos+t):32}static startTime(t){t.startObject(2)}static addUnit(t,e){t.addFieldInt16(0,e,E.MILLISECOND)}static addBitWidth(t,e){t.addFieldInt32(1,e,32)}static endTime(t){return t.endObject()}static createTime(t,e,n){return Je.startTime(t),Je.addUnit(t,e),Je.addBitWidth(t,n),Je.endTime(t)}}var cf=b(()=>{ht();xo()});class Ze{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsTimestamp(t,e){return(e||new Ze).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsTimestamp(t,e){return t.setPosition(t.position()+U),(e||new Ze).__init(t.readInt32(t.position())+t.position(),t)}unit(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):E.SECOND}timezone(t){let e=this.bb.__offset(this.bb_pos,6);return e?this.bb.__string(this.bb_pos+e,t):null}static startTimestamp(t){t.startObject(2)}static addUnit(t,e){t.addFieldInt16(0,e,E.SECOND)}static addTimezone(t,e){t.addFieldOffset(1,e,0)}static endTimestamp(t){return t.endObject()}static createTimestamp(t,e,n){return Ze.startTimestamp(t),Ze.addUnit(t,e),Ze.addTimezone(t,n),Ze.endTimestamp(t)}}var lf=b(()=>{ht();xo()});class Le{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsUnion(t,e){return(e||new Le).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsUnion(t,e){return t.setPosition(t.position()+U),(e||new Le).__init(t.readInt32(t.position())+t.position(),t)}mode(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):yt.Sparse}typeIds(t){let e=this.bb.__offset(this.bb_pos,6);return e?this.bb.readInt32(this.bb.__vector(this.bb_pos+e)+t*4):0}typeIdsLength(){let t=this.bb.__offset(this.bb_pos,6);return t?this.bb.__vector_len(this.bb_pos+t):0}typeIdsArray(){let t=this.bb.__offset(this.bb_pos,6);return t?new Int32Array(this.bb.bytes().buffer,this.bb.bytes().byteOffset+this.bb.__vector(this.bb_pos+t),this.bb.__vector_len(this.bb_pos+t)):null}static startUnion(t){t.startObject(2)}static addMode(t,e){t.addFieldInt16(0,e,yt.Sparse)}static addTypeIds(t,e){t.addFieldOffset(1,e,0)}static createTypeIdsVector(t,e){t.startVector(4,e.length,4);for(let n=e.length-1;n>=0;n--)t.addInt32(e[n]);return t.endVector()}static startTypeIdsVector(t,e){t.startVector(4,e,4)}static endUnion(t){return t.endObject()}static createUnion(t,e,n){return Le.startUnion(t),Le.addMode(t,e),Le.addTypeIds(t,n),Le.endUnion(t)}}var uf=b(()=>{ht();qu()});class _i{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsUtf8(t,e){return(e||new _i).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsUtf8(t,e){return t.setPosition(t.position()+U),(e||new _i).__init(t.readInt32(t.position())+t.position(),t)}static startUtf8(t){t.startObject(0)}static endUtf8(t){return t.endObject()}static createUtf8(t){return _i.startUtf8(t),_i.endUtf8(t)}}var yh=b(()=>{ht()});class gi{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsUtf8View(t,e){return(e||new gi).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsUtf8View(t,e){return t.setPosition(t.position()+U),(e||new gi).__init(t.readInt32(t.position())+t.position(),t)}static startUtf8View(t){t.startObject(0)}static endUtf8View(t){return t.endObject()}static createUtf8View(t){return gi.startUtf8View(t),gi.endUtf8View(t)}}var _h=b(()=>{ht()});var Dt;var ff=b(()=>{(function(t){t[t.NONE=0]="NONE",t[t.Null=1]="Null",t[t.Int=2]="Int",t[t.FloatingPoint=3]="FloatingPoint",t[t.Binary=4]="Binary",t[t.Utf8=5]="Utf8",t[t.Bool=6]="Bool",t[t.Decimal=7]="Decimal",t[t.Date=8]="Date",t[t.Time=9]="Time",t[t.Timestamp=10]="Timestamp",t[t.Interval=11]="Interval",t[t.List=12]="List",t[t.Struct_=13]="Struct_",t[t.Union=14]="Union",t[t.FixedSizeBinary=15]="FixedSizeBinary",t[t.FixedSizeList=16]="FixedSizeList",t[t.Map=17]="Map",t[t.Duration=18]="Duration",t[t.LargeBinary=19]="LargeBinary",t[t.LargeUtf8=20]="LargeUtf8",t[t.LargeList=21]="LargeList",t[t.RunEndEncoded=22]="RunEndEncoded",t[t.BinaryView=23]="BinaryView",t[t.Utf8View=24]="Utf8View",t[t.ListView=25]="ListView",t[t.LargeListView=26]="LargeListView"})(Dt||(Dt={}))});class ge{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsField(t,e){return(e||new ge).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsField(t,e){return t.setPosition(t.position()+U),(e||new ge).__init(t.readInt32(t.position())+t.position(),t)}name(t){let e=this.bb.__offset(this.bb_pos,4);return e?this.bb.__string(this.bb_pos+e,t):null}nullable(){let t=this.bb.__offset(this.bb_pos,6);return t?!!this.bb.readInt8(this.bb_pos+t):!1}typeType(){let t=this.bb.__offset(this.bb_pos,8);return t?this.bb.readUint8(this.bb_pos+t):Dt.NONE}type(t){let e=this.bb.__offset(this.bb_pos,10);return e?this.bb.__union(t,this.bb_pos+e):null}dictionary(t){let e=this.bb.__offset(this.bb_pos,12);return e?(t||new Xn).__init(this.bb.__indirect(this.bb_pos+e),this.bb):null}children(t,e){let n=this.bb.__offset(this.bb_pos,14);return n?(e||new ge).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+n)+t*4),this.bb):null}childrenLength(){let t=this.bb.__offset(this.bb_pos,14);return t?this.bb.__vector_len(this.bb_pos+t):0}customMetadata(t,e){let n=this.bb.__offset(this.bb_pos,16);return n?(e||new Nt).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+n)+t*4),this.bb):null}customMetadataLength(){let t=this.bb.__offset(this.bb_pos,16);return t?this.bb.__vector_len(this.bb_pos+t):0}static startField(t){t.startObject(7)}static addName(t,e){t.addFieldOffset(0,e,0)}static addNullable(t,e){t.addFieldInt8(1,+e,0)}static addTypeType(t,e){t.addFieldInt8(2,e,Dt.NONE)}static addType(t,e){t.addFieldOffset(3,e,0)}static addDictionary(t,e){t.addFieldOffset(4,e,0)}static addChildren(t,e){t.addFieldOffset(5,e,0)}static createChildrenVector(t,e){t.startVector(4,e.length,4);for(let n=e.length-1;n>=0;n--)t.addOffset(e[n]);return t.endVector()}static startChildrenVector(t,e){t.startVector(4,e,4)}static addCustomMetadata(t,e){t.addFieldOffset(6,e,0)}static createCustomMetadataVector(t,e){t.startVector(4,e.length,4);for(let n=e.length-1;n>=0;n--)t.addOffset(e[n]);return t.endVector()}static startCustomMetadataVector(t,e){t.startVector(4,e,4)}static endField(t){return t.endObject()}}var df=b(()=>{ht();Ju();wa();ff()});class Kt{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsSchema(t,e){return(e||new Kt).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsSchema(t,e){return t.setPosition(t.position()+U),(e||new Kt).__init(t.readInt32(t.position())+t.position(),t)}endianness(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):ci.Little}fields(t,e){let n=this.bb.__offset(this.bb_pos,6);return n?(e||new ge).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+n)+t*4),this.bb):null}fieldsLength(){let t=this.bb.__offset(this.bb_pos,6);return t?this.bb.__vector_len(this.bb_pos+t):0}customMetadata(t,e){let n=this.bb.__offset(this.bb_pos,8);return n?(e||new Nt).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+n)+t*4),this.bb):null}customMetadataLength(){let t=this.bb.__offset(this.bb_pos,8);return t?this.bb.__vector_len(this.bb_pos+t):0}features(t){let e=this.bb.__offset(this.bb_pos,10);return e?this.bb.readInt64(this.bb.__vector(this.bb_pos+e)+t*8):BigInt(0)}featuresLength(){let t=this.bb.__offset(this.bb_pos,10);return t?this.bb.__vector_len(this.bb_pos+t):0}static startSchema(t){t.startObject(4)}static addEndianness(t,e){t.addFieldInt16(0,e,ci.Little)}static addFields(t,e){t.addFieldOffset(1,e,0)}static createFieldsVector(t,e){t.startVector(4,e.length,4);for(let n=e.length-1;n>=0;n--)t.addOffset(e[n]);return t.endVector()}static startFieldsVector(t,e){t.startVector(4,e,4)}static addCustomMetadata(t,e){t.addFieldOffset(2,e,0)}static createCustomMetadataVector(t,e){t.startVector(4,e.length,4);for(let n=e.length-1;n>=0;n--)t.addOffset(e[n]);return t.endVector()}static startCustomMetadataVector(t,e){t.startVector(4,e,4)}static addFeatures(t,e){t.addFieldOffset(3,e,0)}static createFeaturesVector(t,e){t.startVector(8,e.length,8);for(let n=e.length-1;n>=0;n--)t.addInt64(e[n]);return t.endVector()}static startFeaturesVector(t,e){t.startVector(8,e,8)}static endSchema(t){return t.endObject()}static createSchema(t,e,n,r,a){return Kt.startSchema(t),Kt.addEndianness(t,e),Kt.addFields(t,n),Kt.addCustomMetadata(t,r),Kt.addFeatures(t,a),Kt.endSchema(t)}}var pf=b(()=>{ht();Xu();df();wa()});var pt;var hf=b(()=>{(function(t){t[t.NONE=0]="NONE",t[t.Schema=1]="Schema",t[t.DictionaryBatch=2]="DictionaryBatch",t[t.RecordBatch=3]="RecordBatch",t[t.Tensor=4]="Tensor",t[t.SparseTensor=5]="SparseTensor"})(pt||(pt={}))});var y,tn;var ne=b(()=>{Js();qu();zu();ju();xo();$u();hf();(function(t){t[t.NONE=0]="NONE",t[t.Null=1]="Null",t[t.Int=2]="Int",t[t.Float=3]="Float",t[t.Binary=4]="Binary",t[t.Utf8=5]="Utf8",t[t.Bool=6]="Bool",t[t.Decimal=7]="Decimal",t[t.Date=8]="Date",t[t.Time=9]="Time",t[t.Timestamp=10]="Timestamp",t[t.Interval=11]="Interval",t[t.List=12]="List",t[t.Struct=13]="Struct",t[t.Union=14]="Union",t[t.FixedSizeBinary=15]="FixedSizeBinary",t[t.FixedSizeList=16]="FixedSizeList",t[t.Map=17]="Map",t[t.Duration=18]="Duration",t[t.LargeBinary=19]="LargeBinary",t[t.LargeUtf8=20]="LargeUtf8",t[t.BinaryView=23]="BinaryView",t[t.Utf8View=24]="Utf8View",t[t.Dictionary=-1]="Dictionary",t[t.Int8=-2]="Int8",t[t.Int16=-3]="Int16",t[t.Int32=-4]="Int32",t[t.Int64=-5]="Int64",t[t.Uint8=-6]="Uint8",t[t.Uint16=-7]="Uint16",t[t.Uint32=-8]="Uint32",t[t.Uint64=-9]="Uint64",t[t.Float16=-10]="Float16",t[t.Float32=-11]="Float32",t[t.Float64=-12]="Float64",t[t.DateDay=-13]="DateDay",t[t.DateMillisecond=-14]="DateMillisecond",t[t.TimestampSecond=-15]="TimestampSecond",t[t.TimestampMillisecond=-16]="TimestampMillisecond",t[t.TimestampMicrosecond=-17]="TimestampMicrosecond",t[t.TimestampNanosecond=-18]="TimestampNanosecond",t[t.TimeSecond=-19]="TimeSecond",t[t.TimeMillisecond=-20]="TimeMillisecond",t[t.TimeMicrosecond=-21]="TimeMicrosecond",t[t.TimeNanosecond=-22]="TimeNanosecond",t[t.DenseUnion=-23]="DenseUnion",t[t.SparseUnion=-24]="SparseUnion",t[t.IntervalDayTime=-25]="IntervalDayTime",t[t.IntervalYearMonth=-26]="IntervalYearMonth",t[t.DurationSecond=-27]="DurationSecond",t[t.DurationMillisecond=-28]="DurationMillisecond",t[t.DurationMicrosecond=-29]="DurationMicrosecond",t[t.DurationNanosecond=-30]="DurationNanosecond",t[t.IntervalMonthDayNano=-31]="IntervalMonthDayNano"})(y||(y={}));(function(t){t[t.OFFSET=0]="OFFSET",t[t.DATA=1]="DATA",t[t.VALIDITY=2]="VALIDITY",t[t.TYPE=3]="TYPE"})(tn||(tn={}))});var mf={};mr(mf,{valueToString:()=>Jn});function Jn(t){if(t===null)return"null";if(t===void 0)return"undefined";switch(typeof t){case"number":return`${t}`;case"bigint":return`${t}`;case"string":return`"${t}"`}if(typeof t[Symbol.toPrimitive]==="function")return t[Symbol.toPrimitive]("string");if(ArrayBuffer.isView(t)){if(t instanceof BigInt64Array||t instanceof BigUint64Array)return`[${[...t].map((e)=>Jn(e))}]`;return`[${t}]`}return ArrayBuffer.isView(t)?`[${t}]`:JSON.stringify(t,(e,n)=>typeof n==="bigint"?`${n}`:n)}function gt(t){if(typeof t==="bigint"&&(tNumber.MAX_SAFE_INTEGER))throw TypeError(`${t} is not safe to convert to a number.`);return Number(t)}function yf(t,e){return gt(t/e)+gt(t%e)/gt(e)}var bf={};mr(bf,{BN:()=>Bo,bigNumToBigInt:()=>bh,bigNumToNumber:()=>gf,bigNumToString:()=>Aa,isArrowBigNumSymbol:()=>gh});function Zn(t,...e){if(e.length===0)return Object.setPrototypeOf(ut(this.TypedArray,t),this.constructor.prototype);return Object.setPrototypeOf(new this.TypedArray(t,...e),this.constructor.prototype)}function xa(...t){return Zn.apply(this,t)}function Sa(...t){return Zn.apply(this,t)}function Do(...t){return Zn.apply(this,t)}function gf(t,e){let{buffer:n,byteOffset:r,byteLength:a,signed:o}=t,s=new BigUint64Array(n,r,a/8),c=o&&s.at(-1)&BigInt(1)<0){let p=BigInt("1".padEnd(e+1,"0")),_=l/p,g=c?-(l%p):l%p,w=gt(_),I=`${g}`.padStart(e,"0");return+`${c&&w===0?"-":""}${w}.${I}`}return gt(l)}function Aa(t){if(t.byteLength===8)return`${new t.BigIntArray(t.buffer,t.byteOffset,1)[0]}`;if(!t.signed)return _f(t);let e=new Uint16Array(t.buffer,t.byteOffset,t.byteLength/2);if(new Int16Array([e.at(-1)])[0]>=0)return _f(t);e=e.slice();let r=1;for(let o=0;o{oe();gh=Symbol.for("isArrowBigNum");Zn.prototype[gh]=!0;Zn.prototype.toJSON=function(){return`"${Aa(this)}"`};Zn.prototype.valueOf=function(t){return gf(this,t)};Zn.prototype.toString=function(){return Aa(this)};Zn.prototype[Symbol.toPrimitive]=function(t="default"){switch(t){case"number":return gf(this);case"string":return Aa(this);case"default":return bh(this)}return Aa(this)};Object.setPrototypeOf(xa.prototype,Object.create(Int32Array.prototype));Object.setPrototypeOf(Sa.prototype,Object.create(Uint32Array.prototype));Object.setPrototypeOf(Do.prototype,Object.create(Uint32Array.prototype));Object.assign(xa.prototype,Zn.prototype,{constructor:xa,signed:!0,TypedArray:Int32Array,BigIntArray:BigInt64Array});Object.assign(Sa.prototype,Zn.prototype,{constructor:Sa,signed:!1,TypedArray:Uint32Array,BigIntArray:BigUint64Array});Object.assign(Do.prototype,Zn.prototype,{constructor:Do,signed:!0,TypedArray:Uint32Array,BigIntArray:BigUint64Array});vw=BigInt(4294967296)*BigInt(4294967296),Ew=vw-BigInt(1)});class S{static isDataType(t){return(t===null||t===void 0?void 0:t[$h])===!0}static isNull(t){return(t===null||t===void 0?void 0:t.typeId)===y.Null}static isInt(t){return(t===null||t===void 0?void 0:t.typeId)===y.Int}static isFloat(t){return(t===null||t===void 0?void 0:t.typeId)===y.Float}static isBinary(t){return(t===null||t===void 0?void 0:t.typeId)===y.Binary}static isBinaryView(t){return(t===null||t===void 0?void 0:t.typeId)===y.BinaryView}static isLargeBinary(t){return(t===null||t===void 0?void 0:t.typeId)===y.LargeBinary}static isUtf8(t){return(t===null||t===void 0?void 0:t.typeId)===y.Utf8}static isUtf8View(t){return(t===null||t===void 0?void 0:t.typeId)===y.Utf8View}static isLargeUtf8(t){return(t===null||t===void 0?void 0:t.typeId)===y.LargeUtf8}static isBool(t){return(t===null||t===void 0?void 0:t.typeId)===y.Bool}static isDecimal(t){return(t===null||t===void 0?void 0:t.typeId)===y.Decimal}static isDate(t){return(t===null||t===void 0?void 0:t.typeId)===y.Date}static isTime(t){return(t===null||t===void 0?void 0:t.typeId)===y.Time}static isTimestamp(t){return(t===null||t===void 0?void 0:t.typeId)===y.Timestamp}static isInterval(t){return(t===null||t===void 0?void 0:t.typeId)===y.Interval}static isDuration(t){return(t===null||t===void 0?void 0:t.typeId)===y.Duration}static isList(t){return(t===null||t===void 0?void 0:t.typeId)===y.List}static isStruct(t){return(t===null||t===void 0?void 0:t.typeId)===y.Struct}static isUnion(t){return(t===null||t===void 0?void 0:t.typeId)===y.Union}static isFixedSizeBinary(t){return(t===null||t===void 0?void 0:t.typeId)===y.FixedSizeBinary}static isFixedSizeList(t){return(t===null||t===void 0?void 0:t.typeId)===y.FixedSizeList}static isMap(t){return(t===null||t===void 0?void 0:t.typeId)===y.Map}static isDictionary(t){return(t===null||t===void 0?void 0:t.typeId)===y.Dictionary}static isDenseUnion(t){return S.isUnion(t)&&t.mode===yt.Dense}static isSparseUnion(t){return S.isUnion(t)&&t.mode===yt.Sparse}constructor(t){this.typeId=t}}function gn(t){let e=t;switch(t.typeId){case y.Decimal:return t.bitWidth/32;case y.Interval:{if(e.unit===It.MONTH_DAY_NANO)return 4;return 1+e.unit}case y.BinaryView:case y.Utf8View:return 16;case y.FixedSizeList:return e.listSize;case y.FixedSizeBinary:return e.byteWidth;default:return 1}}var wh,xh,Sh,Ah,Ih,Th,vh,Eh,Dh,Bh,Ch,Rh,Oh,Nh,Fh,Uh,Mh,Lh,Ph,kh,Vh,qh,zh,jh,$h,zt,se,kr,Vr,Ee,De,qr,zr,jr,qn,Mn,Ia,$r,_n,ze,ct,Ln,Gt,hn,Pn,jt,kn,tr,er,en,Co,nr,rr,ve,it,mn,Ro,bi,Vn,yn,nn,Dw,be;var Ft=b(()=>{ne();$h=Symbol.for("apache-arrow/DataType");wh=Symbol.toStringTag;S[wh]=((t)=>(t.children=null,t.ArrayType=Array,t.OffsetArrayType=Int32Array,t[$h]=!0,t[Symbol.toStringTag]="DataType"))(S.prototype);zt=class zt extends S{constructor(){super(y.Null)}toString(){return"Null"}};xh=Symbol.toStringTag;zt[xh]=((t)=>t[Symbol.toStringTag]="Null")(zt.prototype);se=class se extends S{constructor(t,e){super(y.Int);this.isSigned=t,this.bitWidth=e}get ArrayType(){switch(this.bitWidth){case 8:return this.isSigned?Int8Array:Uint8Array;case 16:return this.isSigned?Int16Array:Uint16Array;case 32:return this.isSigned?Int32Array:Uint32Array;case 64:return this.isSigned?BigInt64Array:BigUint64Array}throw Error(`Unrecognized ${this[Symbol.toStringTag]} type`)}toString(){return`${this.isSigned?"I":"Ui"}nt${this.bitWidth}`}};Sh=Symbol.toStringTag;se[Sh]=((t)=>(t.isSigned=null,t.bitWidth=null,t[Symbol.toStringTag]="Int"))(se.prototype);kr=class kr extends se{constructor(){super(!0,8)}get ArrayType(){return Int8Array}};Vr=class Vr extends se{constructor(){super(!0,16)}get ArrayType(){return Int16Array}};Ee=class Ee extends se{constructor(){super(!0,32)}get ArrayType(){return Int32Array}};De=class De extends se{constructor(){super(!0,64)}get ArrayType(){return BigInt64Array}};qr=class qr extends se{constructor(){super(!1,8)}get ArrayType(){return Uint8Array}};zr=class zr extends se{constructor(){super(!1,16)}get ArrayType(){return Uint16Array}};jr=class jr extends se{constructor(){super(!1,32)}get ArrayType(){return Uint32Array}};qn=class qn extends se{constructor(){super(!1,64)}get ArrayType(){return BigUint64Array}};Object.defineProperty(kr.prototype,"ArrayType",{value:Int8Array});Object.defineProperty(Vr.prototype,"ArrayType",{value:Int16Array});Object.defineProperty(Ee.prototype,"ArrayType",{value:Int32Array});Object.defineProperty(De.prototype,"ArrayType",{value:BigInt64Array});Object.defineProperty(qr.prototype,"ArrayType",{value:Uint8Array});Object.defineProperty(zr.prototype,"ArrayType",{value:Uint16Array});Object.defineProperty(jr.prototype,"ArrayType",{value:Uint32Array});Object.defineProperty(qn.prototype,"ArrayType",{value:BigUint64Array});Mn=class Mn extends S{constructor(t){super(y.Float);this.precision=t}get ArrayType(){switch(this.precision){case Rt.HALF:return Uint16Array;case Rt.SINGLE:return Float32Array;case Rt.DOUBLE:return Float64Array}throw Error(`Unrecognized ${this[Symbol.toStringTag]} type`)}toString(){return`Float${this.precision<<5||16}`}};Ah=Symbol.toStringTag;Mn[Ah]=((t)=>(t.precision=null,t[Symbol.toStringTag]="Float"))(Mn.prototype);Ia=class Ia extends Mn{constructor(){super(Rt.HALF)}};$r=class $r extends Mn{constructor(){super(Rt.SINGLE)}};_n=class _n extends Mn{constructor(){super(Rt.DOUBLE)}};Object.defineProperty(Ia.prototype,"ArrayType",{value:Uint16Array});Object.defineProperty($r.prototype,"ArrayType",{value:Float32Array});Object.defineProperty(_n.prototype,"ArrayType",{value:Float64Array});ze=class ze extends S{constructor(){super(y.Binary)}toString(){return"Binary"}};Ih=Symbol.toStringTag;ze[Ih]=((t)=>(t.ArrayType=Uint8Array,t[Symbol.toStringTag]="Binary"))(ze.prototype);ct=class ct extends S{constructor(){super(y.BinaryView)}toString(){return"BinaryView"}};Th=Symbol.toStringTag;ct.ELEMENT_WIDTH=16;ct.INLINE_CAPACITY=12;ct.LENGTH_OFFSET=0;ct.INLINE_OFFSET=4;ct.BUFFER_INDEX_OFFSET=8;ct.BUFFER_OFFSET_OFFSET=12;ct[Th]=((t)=>(t.ArrayType=Uint8Array,t[Symbol.toStringTag]="BinaryView"))(ct.prototype);Ln=class Ln extends S{constructor(){super(y.LargeBinary)}toString(){return"LargeBinary"}};vh=Symbol.toStringTag;Ln[vh]=((t)=>(t.ArrayType=Uint8Array,t.OffsetArrayType=BigInt64Array,t[Symbol.toStringTag]="LargeBinary"))(Ln.prototype);Gt=class Gt extends S{constructor(){super(y.Utf8)}toString(){return"Utf8"}};Eh=Symbol.toStringTag;Gt[Eh]=((t)=>(t.ArrayType=Uint8Array,t[Symbol.toStringTag]="Utf8"))(Gt.prototype);hn=class hn extends S{constructor(){super(y.Utf8View)}toString(){return"Utf8View"}};Dh=Symbol.toStringTag;hn.ELEMENT_WIDTH=ct.ELEMENT_WIDTH;hn.INLINE_CAPACITY=ct.INLINE_CAPACITY;hn[Dh]=((t)=>(t.ArrayType=Uint8Array,t[Symbol.toStringTag]="Utf8View"))(hn.prototype);Pn=class Pn extends S{constructor(){super(y.LargeUtf8)}toString(){return"LargeUtf8"}};Bh=Symbol.toStringTag;Pn[Bh]=((t)=>(t.ArrayType=Uint8Array,t.OffsetArrayType=BigInt64Array,t[Symbol.toStringTag]="LargeUtf8"))(Pn.prototype);jt=class jt extends S{constructor(){super(y.Bool)}toString(){return"Bool"}};Ch=Symbol.toStringTag;jt[Ch]=((t)=>(t.ArrayType=Uint8Array,t[Symbol.toStringTag]="Bool"))(jt.prototype);kn=class kn extends S{constructor(t,e,n=128){super(y.Decimal);this.scale=t,this.precision=e,this.bitWidth=n}toString(){return`Decimal[${this.precision}e${this.scale>0?"+":""}${this.scale}]`}};Rh=Symbol.toStringTag;kn[Rh]=((t)=>(t.scale=null,t.precision=null,t.ArrayType=Uint32Array,t[Symbol.toStringTag]="Decimal"))(kn.prototype);tr=class tr extends S{constructor(t){super(y.Date);this.unit=t}toString(){return`Date${(this.unit+1)*32}<${vt[this.unit]}>`}get ArrayType(){return this.unit===vt.DAY?Int32Array:BigInt64Array}};Oh=Symbol.toStringTag;tr[Oh]=((t)=>(t.unit=null,t[Symbol.toStringTag]="Date"))(tr.prototype);er=class er extends S{constructor(t,e){super(y.Time);this.unit=t,this.bitWidth=e}toString(){return`Time${this.bitWidth}<${E[this.unit]}>`}get ArrayType(){switch(this.bitWidth){case 32:return Int32Array;case 64:return BigInt64Array}throw Error(`Unrecognized ${this[Symbol.toStringTag]} type`)}};Nh=Symbol.toStringTag;er[Nh]=((t)=>(t.unit=null,t.bitWidth=null,t[Symbol.toStringTag]="Time"))(er.prototype);en=class en extends S{constructor(t,e){super(y.Timestamp);this.unit=t,this.timezone=e}toString(){return`Timestamp<${E[this.unit]}${this.timezone?`, ${this.timezone}`:""}>`}};Fh=Symbol.toStringTag;en[Fh]=((t)=>(t.unit=null,t.timezone=null,t.ArrayType=BigInt64Array,t[Symbol.toStringTag]="Timestamp"))(en.prototype);Co=class Co extends en{constructor(t){super(E.MILLISECOND,t)}};nr=class nr extends S{constructor(t){super(y.Interval);this.unit=t}toString(){return`Interval<${It[this.unit]}>`}};Uh=Symbol.toStringTag;nr[Uh]=((t)=>(t.unit=null,t.ArrayType=Int32Array,t[Symbol.toStringTag]="Interval"))(nr.prototype);rr=class rr extends S{constructor(t){super(y.Duration);this.unit=t}toString(){return`Duration<${E[this.unit]}>`}};Mh=Symbol.toStringTag;rr[Mh]=((t)=>(t.unit=null,t.ArrayType=BigInt64Array,t[Symbol.toStringTag]="Duration"))(rr.prototype);ve=class ve extends S{constructor(t){super(y.List);this.children=[t]}toString(){return`List<${this.valueType}>`}get valueType(){return this.children[0].type}get valueField(){return this.children[0]}get ArrayType(){return this.valueType.ArrayType}};Lh=Symbol.toStringTag;ve[Lh]=((t)=>(t.children=null,t[Symbol.toStringTag]="List"))(ve.prototype);it=class it extends S{constructor(t){super(y.Struct);this.children=t}toString(){return`Struct<{${this.children.map((t)=>`${t.name}:${t.type}`).join(", ")}}>`}};Ph=Symbol.toStringTag;it[Ph]=((t)=>(t.children=null,t[Symbol.toStringTag]="Struct"))(it.prototype);mn=class mn extends S{constructor(t,e,n){super(y.Union);this.mode=t,this.children=n,this.typeIds=e=Int32Array.from(e),this.typeIdToChildIndex=e.reduce((r,a,o)=>(r[a]=o)&&r||r,Object.create(null))}toString(){return`${this[Symbol.toStringTag]}<${this.children.map((t)=>`${t.type}`).join(" | ")}>`}};kh=Symbol.toStringTag;mn[kh]=((t)=>(t.mode=null,t.typeIds=null,t.children=null,t.typeIdToChildIndex=null,t.ArrayType=Int8Array,t[Symbol.toStringTag]="Union"))(mn.prototype);Ro=class Ro extends mn{constructor(t,e){super(yt.Dense,t,e)}};bi=class bi extends mn{constructor(t,e){super(yt.Sparse,t,e)}};Vn=class Vn extends S{constructor(t){super(y.FixedSizeBinary);this.byteWidth=t}toString(){return`FixedSizeBinary[${this.byteWidth}]`}};Vh=Symbol.toStringTag;Vn[Vh]=((t)=>(t.byteWidth=null,t.ArrayType=Uint8Array,t[Symbol.toStringTag]="FixedSizeBinary"))(Vn.prototype);yn=class yn extends S{constructor(t,e){super(y.FixedSizeList);this.listSize=t,this.children=[e]}get valueType(){return this.children[0].type}get valueField(){return this.children[0]}get ArrayType(){return this.valueType.ArrayType}toString(){return`FixedSizeList[${this.listSize}]<${this.valueType}>`}};qh=Symbol.toStringTag;yn[qh]=((t)=>(t.children=null,t.listSize=null,t[Symbol.toStringTag]="FixedSizeList"))(yn.prototype);nn=class nn extends S{constructor(t,e=!1){var n,r,a;super(y.Map);if(this.children=[t],this.keysSorted=e,t){if(t.name="entries",(n=t===null||t===void 0?void 0:t.type)===null||n===void 0?void 0:n.children){let o=(r=t===null||t===void 0?void 0:t.type)===null||r===void 0?void 0:r.children[0];if(o)o.name="key";let s=(a=t===null||t===void 0?void 0:t.type)===null||a===void 0?void 0:a.children[1];if(s)s.name="value"}}}get keyType(){return this.children[0].type.children[0].type}get valueType(){return this.children[0].type.children[1].type}get childType(){return this.children[0].type}toString(){return`Map<{${this.children[0].type.children.map((t)=>`${t.name}:${t.type}`).join(", ")}}>`}};zh=Symbol.toStringTag;nn[zh]=((t)=>(t.children=null,t.keysSorted=null,t[Symbol.toStringTag]="Map_"))(nn.prototype);Dw=((t)=>()=>++t)(-1);be=class be extends S{constructor(t,e,n,r){super(y.Dictionary);this.indices=e,this.dictionary=t,this.isOrdered=r||!1,this.id=n==null?Dw():gt(n)}get children(){return this.dictionary.children}get valueType(){return this.dictionary}get ArrayType(){return this.dictionary.ArrayType}toString(){return`Dictionary<${this.indices}, ${this.dictionary}>`}};jh=Symbol.toStringTag;be[jh]=((t)=>(t.id=null,t.indices=null,t.isOrdered=null,t.dictionary=null,t[Symbol.toStringTag]="Dictionary"))(be.prototype)});class K{visitMany(t,...e){return t.map((n,r)=>this.visit(n,...e.map((a)=>a[r])))}visit(...t){return this.getVisitFn(t[0],!1).apply(this,t)}getVisitFn(t,e=!0){return Bw(this,t,e)}getVisitFnByTypeId(t,e=!0){return Ta(this,t,e)}visitNull(t,...e){return null}visitBool(t,...e){return null}visitInt(t,...e){return null}visitFloat(t,...e){return null}visitUtf8(t,...e){return null}visitLargeUtf8(t,...e){return null}visitUtf8View(t,...e){return null}visitBinary(t,...e){return null}visitLargeBinary(t,...e){return null}visitBinaryView(t,...e){return null}visitFixedSizeBinary(t,...e){return null}visitDate(t,...e){return null}visitTimestamp(t,...e){return null}visitTime(t,...e){return null}visitDecimal(t,...e){return null}visitList(t,...e){return null}visitStruct(t,...e){return null}visitUnion(t,...e){return null}visitDictionary(t,...e){return null}visitInterval(t,...e){return null}visitDuration(t,...e){return null}visitFixedSizeList(t,...e){return null}visitMap(t,...e){return null}}function Bw(t,e,n=!0){if(typeof e==="number")return Ta(t,e,n);if(typeof e==="string"&&e in y)return Ta(t,y[e],n);if(e&&e instanceof S)return Ta(t,Yh(e),n);if((e===null||e===void 0?void 0:e.type)&&e.type instanceof S)return Ta(t,Yh(e.type),n);return Ta(t,y.NONE,n)}function Ta(t,e,n=!0){let r=null;switch(e){case y.Null:r=t.visitNull;break;case y.Bool:r=t.visitBool;break;case y.Int:r=t.visitInt;break;case y.Int8:r=t.visitInt8||t.visitInt;break;case y.Int16:r=t.visitInt16||t.visitInt;break;case y.Int32:r=t.visitInt32||t.visitInt;break;case y.Int64:r=t.visitInt64||t.visitInt;break;case y.Uint8:r=t.visitUint8||t.visitInt;break;case y.Uint16:r=t.visitUint16||t.visitInt;break;case y.Uint32:r=t.visitUint32||t.visitInt;break;case y.Uint64:r=t.visitUint64||t.visitInt;break;case y.Float:r=t.visitFloat;break;case y.Float16:r=t.visitFloat16||t.visitFloat;break;case y.Float32:r=t.visitFloat32||t.visitFloat;break;case y.Float64:r=t.visitFloat64||t.visitFloat;break;case y.Utf8:r=t.visitUtf8;break;case y.LargeUtf8:r=t.visitLargeUtf8;break;case y.Utf8View:r=t.visitUtf8View||t.visitUtf8;break;case y.Binary:r=t.visitBinary;break;case y.LargeBinary:r=t.visitLargeBinary;break;case y.BinaryView:r=t.visitBinaryView||t.visitBinary;break;case y.FixedSizeBinary:r=t.visitFixedSizeBinary;break;case y.Date:r=t.visitDate;break;case y.DateDay:r=t.visitDateDay||t.visitDate;break;case y.DateMillisecond:r=t.visitDateMillisecond||t.visitDate;break;case y.Timestamp:r=t.visitTimestamp;break;case y.TimestampSecond:r=t.visitTimestampSecond||t.visitTimestamp;break;case y.TimestampMillisecond:r=t.visitTimestampMillisecond||t.visitTimestamp;break;case y.TimestampMicrosecond:r=t.visitTimestampMicrosecond||t.visitTimestamp;break;case y.TimestampNanosecond:r=t.visitTimestampNanosecond||t.visitTimestamp;break;case y.Time:r=t.visitTime;break;case y.TimeSecond:r=t.visitTimeSecond||t.visitTime;break;case y.TimeMillisecond:r=t.visitTimeMillisecond||t.visitTime;break;case y.TimeMicrosecond:r=t.visitTimeMicrosecond||t.visitTime;break;case y.TimeNanosecond:r=t.visitTimeNanosecond||t.visitTime;break;case y.Decimal:r=t.visitDecimal;break;case y.List:r=t.visitList;break;case y.Struct:r=t.visitStruct;break;case y.Union:r=t.visitUnion;break;case y.DenseUnion:r=t.visitDenseUnion||t.visitUnion;break;case y.SparseUnion:r=t.visitSparseUnion||t.visitUnion;break;case y.Dictionary:r=t.visitDictionary;break;case y.Interval:r=t.visitInterval;break;case y.IntervalDayTime:r=t.visitIntervalDayTime||t.visitInterval;break;case y.IntervalYearMonth:r=t.visitIntervalYearMonth||t.visitInterval;break;case y.IntervalMonthDayNano:r=t.visitIntervalMonthDayNano||t.visitInterval;break;case y.Duration:r=t.visitDuration;break;case y.DurationSecond:r=t.visitDurationSecond||t.visitDuration;break;case y.DurationMillisecond:r=t.visitDurationMillisecond||t.visitDuration;break;case y.DurationMicrosecond:r=t.visitDurationMicrosecond||t.visitDuration;break;case y.DurationNanosecond:r=t.visitDurationNanosecond||t.visitDuration;break;case y.FixedSizeList:r=t.visitFixedSizeList;break;case y.Map:r=t.visitMap;break}if(typeof r==="function")return r;if(!n)return()=>null;throw Error(`Unrecognized type '${y[e]}'`)}function Yh(t){switch(t.typeId){case y.Null:return y.Null;case y.Int:{let{bitWidth:e,isSigned:n}=t;switch(e){case 8:return n?y.Int8:y.Uint8;case 16:return n?y.Int16:y.Uint16;case 32:return n?y.Int32:y.Uint32;case 64:return n?y.Int64:y.Uint64}return y.Int}case y.Float:switch(t.precision){case Rt.HALF:return y.Float16;case Rt.SINGLE:return y.Float32;case Rt.DOUBLE:return y.Float64}return y.Float;case y.Binary:return y.Binary;case y.LargeBinary:return y.LargeBinary;case y.BinaryView:return y.BinaryView;case y.Utf8:return y.Utf8;case y.LargeUtf8:return y.LargeUtf8;case y.Utf8View:return y.Utf8View;case y.Bool:return y.Bool;case y.Decimal:return y.Decimal;case y.Time:switch(t.unit){case E.SECOND:return y.TimeSecond;case E.MILLISECOND:return y.TimeMillisecond;case E.MICROSECOND:return y.TimeMicrosecond;case E.NANOSECOND:return y.TimeNanosecond}return y.Time;case y.Timestamp:switch(t.unit){case E.SECOND:return y.TimestampSecond;case E.MILLISECOND:return y.TimestampMillisecond;case E.MICROSECOND:return y.TimestampMicrosecond;case E.NANOSECOND:return y.TimestampNanosecond}return y.Timestamp;case y.Date:switch(t.unit){case vt.DAY:return y.DateDay;case vt.MILLISECOND:return y.DateMillisecond}return y.Date;case y.Interval:switch(t.unit){case It.DAY_TIME:return y.IntervalDayTime;case It.YEAR_MONTH:return y.IntervalYearMonth;case It.MONTH_DAY_NANO:return y.IntervalMonthDayNano}return y.Interval;case y.Duration:switch(t.unit){case E.SECOND:return y.DurationSecond;case E.MILLISECOND:return y.DurationMillisecond;case E.MICROSECOND:return y.DurationMicrosecond;case E.NANOSECOND:return y.DurationNanosecond}return y.Duration;case y.Map:return y.Map;case y.List:return y.List;case y.Struct:return y.Struct;case y.Union:switch(t.mode){case yt.Dense:return y.DenseUnion;case yt.Sparse:return y.SparseUnion}return y.Union;case y.FixedSizeBinary:return y.FixedSizeBinary;case y.FixedSizeList:return y.FixedSizeList;case y.Dictionary:return y.Dictionary}throw Error(`Unrecognized type '${y[t.typeId]}'`)}var ir=b(()=>{ne();Ft();K.prototype.visitInt8=null;K.prototype.visitInt16=null;K.prototype.visitInt32=null;K.prototype.visitInt64=null;K.prototype.visitUint8=null;K.prototype.visitUint16=null;K.prototype.visitUint32=null;K.prototype.visitUint64=null;K.prototype.visitFloat16=null;K.prototype.visitFloat32=null;K.prototype.visitFloat64=null;K.prototype.visitDateDay=null;K.prototype.visitDateMillisecond=null;K.prototype.visitTimestampSecond=null;K.prototype.visitTimestampMillisecond=null;K.prototype.visitTimestampMicrosecond=null;K.prototype.visitTimestampNanosecond=null;K.prototype.visitTimeSecond=null;K.prototype.visitTimeMillisecond=null;K.prototype.visitTimeMicrosecond=null;K.prototype.visitTimeNanosecond=null;K.prototype.visitDenseUnion=null;K.prototype.visitSparseUnion=null;K.prototype.visitIntervalDayTime=null;K.prototype.visitIntervalYearMonth=null;K.prototype.visitIntervalMonthDayNano=null;K.prototype.visitDuration=null;K.prototype.visitDurationSecond=null;K.prototype.visitDurationMillisecond=null;K.prototype.visitDurationMicrosecond=null;K.prototype.visitDurationNanosecond=null});var xf={};mr(xf,{float64ToUint16:()=>Oo,uint16ToFloat64:()=>nc});function nc(t){let e=(t&31744)>>10,n=(t&1023)/1024,r=Math.pow(-1,(t&32768)>>15);switch(e){case 31:return r*(n?Number.NaN:1/0);case 0:return r*(n?0.00006103515625*n:0)}return r*Math.pow(2,e-15)*(1+n)}function Oo(t){if(t!==t)return 32256;Kh[0]=t;let e=(va[1]&2147483648)>>16&65535,n=va[1]&2146435072,r=0;if(n>=1089470464)if(va[0]>0)n=31744;else n=(n&2080374784)>>16,r=(va[1]&1048575)>>10;else if(n<=1056964608)r=1048576+(va[1]&1048575),r=1048576+(r<<(n>>20)-998)>>21,n=0;else n=n-1056964608>>10,r=(va[1]&1048575)+512>>10;return e|n|r&65535}var Kh,va;var No=b(()=>{Kh=new Float64Array(1),va=new Uint32Array(Kh.buffer)});function Q(t){return(e,n,r)=>{if(e.setValid(n,r!=null))return t(e,n,r)}}var H,Cw=(t,e,n)=>{t[e]=Math.floor(n/86400000)},Gh=(t,e,n,r)=>{if(n+1{let a=t+n;r?e[a>>3]|=1<>3]&=~(1<{t[e]=n},Sf=({values:t},e,n)=>{t[e]=n},Wh=({values:t},e,n)=>{t[e]=Oo(n)},Ow=(t,e,n)=>{switch(t.type.precision){case Rt.HALF:return Wh(t,e,n);case Rt.SINGLE:case Rt.DOUBLE:return Sf(t,e,n)}},rc=({values:t},e,n)=>{Cw(t,e,n.valueOf())},ic=({values:t},e,n)=>{t[e]=BigInt(n)},Af=({stride:t,values:e},n,r)=>{e.set(r.subarray(0,t),t*n)},Qh=({values:t,valueOffsets:e},n,r)=>Gh(t,e,n,r),Nw=(t)=>{let e=t.variadicBuffers;if(!Array.isArray(e)||Object.isFrozen(e))e=Array.from(e),t.variadicBuffers=e;return e},Xh=(t,e,n)=>{var r,a,o,s;let c=t.values;if(!c)throw Error("BinaryView data is missing view buffer");let l=ct.ELEMENT_WIDTH,f=e*l,p=f+l;if(f<0||p>c.length)throw RangeError(`BinaryView index ${e} out of bounds`);c.fill(0,f,p);let _=new DataView(c.buffer,c.byteOffset+f,l),g=n.length;if(_.setInt32(ct.LENGTH_OFFSET,g,!0),g<=ct.INLINE_CAPACITY){c.set(n,f+ct.INLINE_OFFSET);return}let w=((r=n[0])!==null&&r!==void 0?r:0)|((a=n[1])!==null&&a!==void 0?a:0)<<8|((o=n[2])!==null&&o!==void 0?o:0)<<16|((s=n[3])!==null&&s!==void 0?s:0)<<24;_.setUint32(ct.INLINE_OFFSET,w>>>0,!0);let I=Nw(t),x=n.slice(),C=I.push(x)-1;_.setInt32(ct.BUFFER_INDEX_OFFSET,C,!0),_.setInt32(ct.BUFFER_OFFSET_OFFSET,0,!0)},Fw=(t,e,n)=>{let r=n instanceof Uint8Array?n:new Uint8Array(n);Xh(t,e,r)},Jh=({values:t,valueOffsets:e},n,r)=>Gh(t,e,n,fn(r)),Uw=(t,e,n)=>{let r=fn(n);Xh(t,e,r)},If=(t,e,n)=>{t.type.unit===vt.DAY?rc(t,e,n):ic(t,e,n)},ac=({values:t},e,n)=>{t[e]=BigInt(n/1000)},oc=({values:t},e,n)=>{t[e]=BigInt(n)},sc=({values:t},e,n)=>{t[e]=BigInt(n*1000)},cc=({values:t},e,n)=>{t[e]=BigInt(n*1e6)},Tf=(t,e,n)=>{switch(t.type.unit){case E.SECOND:return ac(t,e,n);case E.MILLISECOND:return oc(t,e,n);case E.MICROSECOND:return sc(t,e,n);case E.NANOSECOND:return cc(t,e,n)}},lc=({values:t},e,n)=>{t[e]=n},uc=({values:t},e,n)=>{t[e]=n},fc=({values:t},e,n)=>{t[e]=n},dc=({values:t},e,n)=>{t[e]=n},vf=(t,e,n)=>{switch(t.type.unit){case E.SECOND:return lc(t,e,n);case E.MILLISECOND:return uc(t,e,n);case E.MICROSECOND:return fc(t,e,n);case E.NANOSECOND:return dc(t,e,n)}},Ef=({values:t,stride:e},n,r)=>{t.set(r.subarray(0,e),e*n)},Mw=(t,e,n)=>{let r=t.children[0],a=t.valueOffsets,o=Be.getVisitFn(r);if(Array.isArray(n))for(let s=-1,c=a[e],l=a[e+1];c{let r=t.children[0],{valueOffsets:a}=t,o=Be.getVisitFn(r),{[e]:s,[e+1]:c}=a,l=n instanceof Map?n.entries():Object.entries(n);for(let f of l)if(o(r,s,f),++s>=c)break},Pw=(t,e)=>(n,r,a,o)=>r&&n(r,t,e[o]),kw=(t,e)=>(n,r,a,o)=>r&&n(r,t,e.get(o)),Vw=(t,e)=>(n,r,a,o)=>r&&n(r,t,e.get(a.name)),qw=(t,e)=>(n,r,a,o)=>r&&n(r,t,e[a.name]),zw=(t,e,n)=>{let r=t.type.children.map((o)=>Be.getVisitFn(o.type)),a=n instanceof Map?Vw(e,n):n instanceof O?kw(e,n):Array.isArray(n)?Pw(e,n):qw(e,n);t.type.children.forEach((o,s)=>a(r[s],t.children[s],o,s))},jw=(t,e,n)=>{t.type.mode===yt.Dense?Zh(t,e,n):tm(t,e,n)},Zh=(t,e,n)=>{let r=t.type.typeIdToChildIndex[t.typeIds[e]],a=t.children[r];Be.visit(a,t.valueOffsets[e],n)},tm=(t,e,n)=>{let r=t.type.typeIdToChildIndex[t.typeIds[e]],a=t.children[r];Be.visit(a,e,n)},$w=(t,e,n)=>{var r;(r=t.dictionary)===null||r===void 0||r.set(t.values[e],n)},Df=(t,e,n)=>{switch(t.type.unit){case It.YEAR_MONTH:return hc(t,e,n);case It.DAY_TIME:return pc(t,e,n);case It.MONTH_DAY_NANO:return mc(t,e,n)}},pc=({values:t},e,n)=>{t.set(n.subarray(0,2),2*e)},hc=({values:t},e,n)=>{t[e]=n[0]*12+n[1]%12},mc=({values:t,stride:e},n,r)=>{t.set(r.subarray(0,e),e*n)},yc=({values:t},e,n)=>{t[e]=n},_c=({values:t},e,n)=>{t[e]=n},gc=({values:t},e,n)=>{t[e]=n},bc=({values:t},e,n)=>{t[e]=n},Bf=(t,e,n)=>{switch(t.type.unit){case E.SECOND:return yc(t,e,n);case E.MILLISECOND:return _c(t,e,n);case E.MICROSECOND:return gc(t,e,n);case E.NANOSECOND:return bc(t,e,n)}},Hw=(t,e,n)=>{let{stride:r}=t,a=t.children[0],o=Be.getVisitFn(a);if(Array.isArray(n))for(let s=-1,c=e*r;++s{je();ir();Pr();No();ne();Ft();H=class H extends K{};H.prototype.visitBool=Q(Rw);H.prototype.visitInt=Q(Hr);H.prototype.visitInt8=Q(Hr);H.prototype.visitInt16=Q(Hr);H.prototype.visitInt32=Q(Hr);H.prototype.visitInt64=Q(Hr);H.prototype.visitUint8=Q(Hr);H.prototype.visitUint16=Q(Hr);H.prototype.visitUint32=Q(Hr);H.prototype.visitUint64=Q(Hr);H.prototype.visitFloat=Q(Ow);H.prototype.visitFloat16=Q(Wh);H.prototype.visitFloat32=Q(Sf);H.prototype.visitFloat64=Q(Sf);H.prototype.visitUtf8=Q(Jh);H.prototype.visitLargeUtf8=Q(Jh);H.prototype.visitUtf8View=Q(Uw);H.prototype.visitBinary=Q(Qh);H.prototype.visitLargeBinary=Q(Qh);H.prototype.visitBinaryView=Q(Fw);H.prototype.visitFixedSizeBinary=Q(Af);H.prototype.visitDate=Q(If);H.prototype.visitDateDay=Q(rc);H.prototype.visitDateMillisecond=Q(ic);H.prototype.visitTimestamp=Q(Tf);H.prototype.visitTimestampSecond=Q(ac);H.prototype.visitTimestampMillisecond=Q(oc);H.prototype.visitTimestampMicrosecond=Q(sc);H.prototype.visitTimestampNanosecond=Q(cc);H.prototype.visitTime=Q(vf);H.prototype.visitTimeSecond=Q(lc);H.prototype.visitTimeMillisecond=Q(uc);H.prototype.visitTimeMicrosecond=Q(fc);H.prototype.visitTimeNanosecond=Q(dc);H.prototype.visitDecimal=Q(Ef);H.prototype.visitList=Q(Mw);H.prototype.visitStruct=Q(zw);H.prototype.visitUnion=Q(jw);H.prototype.visitDenseUnion=Q(Zh);H.prototype.visitSparseUnion=Q(tm);H.prototype.visitDictionary=Q($w);H.prototype.visitInterval=Q(Df);H.prototype.visitIntervalDayTime=Q(pc);H.prototype.visitIntervalYearMonth=Q(hc);H.prototype.visitIntervalMonthDayNano=Q(mc);H.prototype.visitDuration=Q(Bf);H.prototype.visitDurationSecond=Q(yc);H.prototype.visitDurationMillisecond=Q(_c);H.prototype.visitDurationMicrosecond=Q(gc);H.prototype.visitDurationNanosecond=Q(bc);H.prototype.visitFixedSizeList=Q(Hw);H.prototype.visitMap=Q(Lw);Be=new H});class nm{isExtensible(){return!1}deleteProperty(){return!1}preventExtensions(){return!0}ownKeys(t){return t[ar].type.children.map((e)=>e.name)}has(t,e){return t[ar].type.children.some((n)=>n.name===e)}getOwnPropertyDescriptor(t,e){if(t[ar].type.children.some((n)=>n.name===e))return{writable:!0,enumerable:!0,configurable:!0};return}get(t,e){if(Reflect.has(t,e))return t[e];let n=t[ar].type.children.findIndex((r)=>r.name===e);if(n!==-1){let r=re.visit(t[ar].children[n],t[Ea]);return Reflect.set(t,e,r),r}}set(t,e,n){let r=t[ar].type.children.findIndex((a)=>a.name===e);if(r!==-1)return Be.visit(t[ar].children[r],t[Ea],n),Reflect.set(t,e,n);else if(Reflect.has(t,e)||typeof e==="symbol")return Reflect.set(t,e,n);return!1}}var ar,Ea,Hi,em,Yw;var Cf=b(()=>{Yi();bn();ar=Symbol.for("parent"),Ea=Symbol.for("rowIndex");Hi=class Hi{constructor(t,e){return this[ar]=t,this[Ea]=e,new Proxy(this,Yw)}toArray(){return Object.values(this.toJSON())}toJSON(){let t=this[Ea],e=this[ar],n=e.type.children,r={};for(let a=-1,o=n.length;++a`${Jn(t)}: ${Jn(e)}`).join(", ")}}`}[Symbol.for("nodejs.util.inspect.custom")](){return this.toString()}[Symbol.iterator](){return new em(this[ar],this[Ea])}};em=class em{constructor(t,e){this.childIndex=0,this.children=t.children,this.rowIndex=e,this.childFields=t.type.children,this.numChildren=this.childFields.length}[Symbol.iterator](){return this}next(){let t=this.childIndex;if(te.getValid(n)?t(e,n):null}var V,Kw=(t,e)=>86400000*t[e],Da=16,Gw=12,Ww=(t,e)=>null,rm=(t,e,n)=>{if(n+1>=e.length)return null;let r=gt(e[n]),a=gt(e[n+1]);return t.subarray(r,a)},Qw=({offset:t,values:e},n)=>{let r=t+n;return(e[r>>3]&1<Kw(t,e),am=({values:t},e)=>gt(t[e]),wi=({stride:t,values:e},n)=>e[t*n],Xw=({stride:t,values:e},n)=>nc(e[t*n]),om=({values:t},e)=>t[e],Jw=({stride:t,values:e},n)=>e.subarray(t*n,t*(n+1)),sm=({values:t,valueOffsets:e},n)=>rm(t,e,n),cm=(t,e)=>{var n;let r=t.values;if(!r)throw Error("BinaryView data is missing view buffer");let a=e*Da,o=a+Da;if(a<0||o>r.length)throw Error(`BinaryView data buffer is too short: expected ${Da} bytes, got ${Math.max(0,r.length-a)}`);let s=r.subarray(a,o);if(s.lengthcm(t,e),lm=({values:t,valueOffsets:e},n)=>{let r=rm(t,e,n);return r!==null?ga(r):null},tx=(t,e)=>{let n=cm(t,e);return ga(n)},ex=({values:t},e)=>t[e],nx=({type:t,values:e},n)=>t.precision!==Rt.HALF?e[n]:nc(e[n]),rx=(t,e)=>t.type.unit===vt.DAY?im(t,e):am(t,e),um=({values:t},e)=>1000*gt(t[e]),fm=({values:t},e)=>gt(t[e]),dm=({values:t},e)=>yf(t[e],BigInt(1000)),pm=({values:t},e)=>yf(t[e],BigInt(1e6)),ix=(t,e)=>{switch(t.type.unit){case E.SECOND:return um(t,e);case E.MILLISECOND:return fm(t,e);case E.MICROSECOND:return dm(t,e);case E.NANOSECOND:return pm(t,e)}},hm=({values:t},e)=>t[e],mm=({values:t},e)=>t[e],ym=({values:t},e)=>t[e],_m=({values:t},e)=>t[e],ax=(t,e)=>{switch(t.type.unit){case E.SECOND:return hm(t,e);case E.MILLISECOND:return mm(t,e);case E.MICROSECOND:return ym(t,e);case E.NANOSECOND:return _m(t,e)}},ox=({values:t,stride:e},n)=>Bo.decimal(t.subarray(e*n,e*(n+1))),sx=(t,e)=>{let{valueOffsets:n,stride:r,children:a}=t,{[e*r]:o,[e*r+1]:s}=n,l=a[0].slice(o,s-o);return new O([l])},cx=(t,e)=>{let{valueOffsets:n,children:r}=t,{[e]:a,[e+1]:o}=n,s=r[0];return new Yr(s.slice(a,o-a))},lx=(t,e)=>new Hi(t,e),ux=(t,e)=>t.type.mode===yt.Dense?gm(t,e):bm(t,e),gm=(t,e)=>{let n=t.type.typeIdToChildIndex[t.typeIds[e]],r=t.children[n];return re.visit(r,t.valueOffsets[e])},bm=(t,e)=>{let n=t.type.typeIdToChildIndex[t.typeIds[e]],r=t.children[n];return re.visit(r,e)},fx=(t,e)=>{var n;return(n=t.dictionary)===null||n===void 0?void 0:n.get(t.values[e])},dx=(t,e)=>t.type.unit===It.MONTH_DAY_NANO?Sm(t,e):t.type.unit===It.DAY_TIME?wm(t,e):xm(t,e),wm=({values:t},e)=>t.subarray(2*e,2*(e+1)),xm=({values:t},e)=>{let n=t[e],r=new Int32Array(2);return r[0]=Math.trunc(n/12),r[1]=Math.trunc(n%12),r},Sm=({values:t},e)=>t.subarray(4*e,4*(e+1)),Am=({values:t},e)=>t[e],Im=({values:t},e)=>t[e],Tm=({values:t},e)=>t[e],vm=({values:t},e)=>t[e],px=(t,e)=>{switch(t.type.unit){case E.SECOND:return Am(t,e);case E.MILLISECOND:return Im(t,e);case E.MICROSECOND:return Tm(t,e);case E.NANOSECOND:return vm(t,e)}},hx=(t,e)=>{let{stride:n,children:r}=t,o=r[0].slice(e*n,n);return new O([o])},re;var Yi=b(()=>{wf();je();ir();wc();Cf();Pr();No();ne();V=class V extends K{};V.prototype.visitNull=G(Ww);V.prototype.visitBool=G(Qw);V.prototype.visitInt=G(ex);V.prototype.visitInt8=G(wi);V.prototype.visitInt16=G(wi);V.prototype.visitInt32=G(wi);V.prototype.visitInt64=G(om);V.prototype.visitUint8=G(wi);V.prototype.visitUint16=G(wi);V.prototype.visitUint32=G(wi);V.prototype.visitUint64=G(om);V.prototype.visitFloat=G(nx);V.prototype.visitFloat16=G(Xw);V.prototype.visitFloat32=G(wi);V.prototype.visitFloat64=G(wi);V.prototype.visitUtf8=G(lm);V.prototype.visitLargeUtf8=G(lm);V.prototype.visitUtf8View=G(tx);V.prototype.visitBinary=G(sm);V.prototype.visitLargeBinary=G(sm);V.prototype.visitBinaryView=G(Zw);V.prototype.visitFixedSizeBinary=G(Jw);V.prototype.visitDate=G(rx);V.prototype.visitDateDay=G(im);V.prototype.visitDateMillisecond=G(am);V.prototype.visitTimestamp=G(ix);V.prototype.visitTimestampSecond=G(um);V.prototype.visitTimestampMillisecond=G(fm);V.prototype.visitTimestampMicrosecond=G(dm);V.prototype.visitTimestampNanosecond=G(pm);V.prototype.visitTime=G(ax);V.prototype.visitTimeSecond=G(hm);V.prototype.visitTimeMillisecond=G(mm);V.prototype.visitTimeMicrosecond=G(ym);V.prototype.visitTimeNanosecond=G(_m);V.prototype.visitDecimal=G(ox);V.prototype.visitList=G(sx);V.prototype.visitStruct=G(lx);V.prototype.visitUnion=G(ux);V.prototype.visitDenseUnion=G(gm);V.prototype.visitSparseUnion=G(bm);V.prototype.visitDictionary=G(fx);V.prototype.visitInterval=G(dx);V.prototype.visitIntervalDayTime=G(wm);V.prototype.visitIntervalYearMonth=G(xm);V.prototype.visitIntervalMonthDayNano=G(Sm);V.prototype.visitDuration=G(px);V.prototype.visitDurationSecond=G(Am);V.prototype.visitDurationMillisecond=G(Im);V.prototype.visitDurationMicrosecond=G(Tm);V.prototype.visitDurationNanosecond=G(vm);V.prototype.visitFixedSizeList=G(hx);V.prototype.visitMap=G(cx);re=new V});class Dm{isExtensible(){return!1}deleteProperty(){return!1}preventExtensions(){return!0}ownKeys(t){return t[Ba]}has(t,e){return t[Ba].includes(e)}getOwnPropertyDescriptor(t,e){if(t[Ba].indexOf(e)!==-1)return{writable:!0,enumerable:!0,configurable:!0};return}get(t,e){if(Reflect.has(t,e))return t[e];let n=t[Ba].indexOf(e);if(n!==-1){let r=re.visit(Reflect.get(t,Ca),n);return Reflect.set(t,e,r),r}}set(t,e,n){let r=t[Ba].indexOf(e);if(r!==-1)return Be.visit(Reflect.get(t,Ca),r,n),Reflect.set(t,e,n);else if(Reflect.has(t,e))return Reflect.set(t,e,n);return!1}}var xi,Ca,Ba,Rf,Yr,Em;var wc=b(()=>{je();Yi();bn();xi=Symbol.for("keys"),Ca=Symbol.for("vals"),Ba=Symbol.for("kKeysAsStrings"),Rf=Symbol.for("_kKeysAsStrings");Yr=class Yr{constructor(t){return this[xi]=new O([t.children[0]]).memoize(),this[Ca]=t.children[1],new Proxy(this,new Dm)}get[Ba](){return this[Rf]||(this[Rf]=Array.from(this[xi].toArray(),String))}[Symbol.iterator](){return new Em(this[xi],this[Ca])}get size(){return this[xi].length}toArray(){return Object.values(this.toJSON())}toJSON(){let t=this[xi],e=this[Ca],n={};for(let r=-1,a=t.length;++r`${Jn(t)}: ${Jn(e)}`).join(", ")}}`}[Symbol.for("nodejs.util.inspect.custom")](){return this.toString()}};Em=class Em{constructor(t,e){this.keys=t,this.vals=e,this.keyIndex=0,this.numKeys=t.length}[Symbol.iterator](){return this}next(){let t=this.keyIndex;if(t===this.numKeys)return{done:!0,value:null};return this.keyIndex++,{done:!1,value:[this.keys.get(t),re.visit(this.vals,t)]}}};Object.defineProperties(Yr.prototype,{[Symbol.toStringTag]:{enumerable:!1,configurable:!1,value:"Row"},[xi]:{writable:!0,enumerable:!1,configurable:!1,value:null},[Ca]:{writable:!0,enumerable:!1,configurable:!1,value:null},[Rf]:{writable:!0,enumerable:!1,configurable:!1,value:null}})});var Of={};mr(Of,{clampRange:()=>Fo,createElementComparator:()=>Si,wrapIndex:()=>Ki});function Fo(t,e,n,r){let{length:a=0}=t,o=typeof e!=="number"?0:e,s=typeof n!=="number"?a:n;return o<0&&(o=(o%a+a)%a),s<0&&(s=(s%a+a)%a),sa&&(s=a),r?r(t,o,s):[o,s]}function Si(t){if(typeof t!=="object"||t===null){if(Cm(t))return Cm;return(n)=>n===t}if(t instanceof Date){let n=t.valueOf();return(r)=>r instanceof Date?r.valueOf()===n:!1}if(ArrayBuffer.isView(t))return(n)=>n?Pu(t,n):!1;if(t instanceof Map)return yx(t);if(Array.isArray(t))return mx(t);if(t instanceof O)return _x(t);return gx(t,!0)}function mx(t){let e=[];for(let n=-1,r=t.length;++n!1;let r=[];for(let a=-1,o=n.length;++a{if(!n||typeof n!=="object")return!1;switch(n.constructor){case Array:return bx(t,n);case Map:return Rm(t,n,n.keys());case Yr:case Hi:case Object:case void 0:return Rm(t,n,e||Object.keys(n))}return n instanceof O?wx(t,n):!1}}function bx(t,e){let n=t.length;if(e.length!==n)return!1;for(let r=-1;++rt<0?e+t:t,Cm=(t)=>t!==t;var Ra=b(()=>{je();wc();Cf();oe()});var Nf={};mr(Nf,{BitIterator:()=>Oa,getBit:()=>Om,getBool:()=>Ac,packBools:()=>Wi,popcnt_array:()=>Nm,popcnt_bit_range:()=>Uo,popcnt_uint32:()=>Sc,setBool:()=>xx,truncateBitmap:()=>Gi});function Ac(t,e,n,r){return(n&1<>r}function xx(t,e,n){return n?!!(t[e>>3]|=1<>3]&=~(1<0||n.byteLength>3):Wi(new Oa(n,t,e,null,Ac)).subarray(0,r)),a}return n}function Wi(t){let e=[],n=0,r=0,a=0;for(let s of t)if(s&&(a|=1<0)e[n++]=a;let o=new Uint8Array(e.length+7&-8);return o.set(e),o}function Uo(t,e,n){if(n-e<=0)return 0;if(n-e<8){let o=0;for(let s of new Oa(t,e,n-e,t,Om))o+=s;return o}let r=n>>3<<3,a=e+(e%8===0?0:8-e%8);return Uo(t,e,a)+Uo(t,r,n)+Nm(t,a>>3,r-a>>3)}function Nm(t,e,n){let r=0,a=Math.trunc(e),o=new DataView(t.buffer,t.byteOffset,t.byteLength),s=n===void 0?t.byteLength:a+n;while(s-a>=4)r+=Sc(o.getUint32(a)),a+=4;while(s-a>=2)r+=Sc(o.getUint16(a)),a+=2;while(s-a>=1)r+=Sc(o.getUint8(a)),a+=1;return r}function Sc(t){let e=Math.trunc(t);return e=e-(e>>>1&1431655765),e=(e&858993459)+(e>>>2&858993459),(e+(e>>>4)&252645135)*16843009>>>24}var Oa;var Na=b(()=>{Oa=class Oa{constructor(t,e,n,r,a){this.bytes=t,this.length=n,this.context=r,this.get=a,this.bit=e%8,this.byteIndex=e>>3,this.byte=t[this.byteIndex++],this.index=0}next(){if(this.indexe.nullable);else if(S.isDenseUnion(t))return this.children.some((e)=>e.nullable);return this.nullBitmap&&this.nullBitmap.byteLength>0}return!0}get byteLength(){let t=0,{valueOffsets:e,values:n,nullBitmap:r,typeIds:a}=this;return e&&(t+=e.byteLength),n&&(t+=n.byteLength),r&&(t+=r.byteLength),a&&(t+=a.byteLength),t+=this.variadicBuffers.reduce((o,s)=>{var c;return o+((c=s===null||s===void 0?void 0:s.byteLength)!==null&&c!==void 0?c:0)},0),this.children.reduce((o,s)=>o+s.byteLength,t)}get nullCount(){if(S.isUnion(this.type))return this.children.reduce((n,r)=>n+r.nullCount,0);let t=this._nullCount,e;if(t<=Sx&&(e=this.nullBitmap))this._nullCount=t=e.length===0?0:this.length-Uo(e,this.offset,this.offset+this.length);return t}constructor(t,e,n,r,a,o=[],s,c=[]){var l;this.type=t,this.children=o,this.dictionary=s,this.offset=Math.floor(Math.max(e||0,0)),this.length=Math.floor(Math.max(n||0,0)),this._nullCount=Math.floor(Math.max(r||0,-1));let f;if(a instanceof tt)this.stride=a.stride,this.values=a.values,this.typeIds=a.typeIds,this.nullBitmap=a.nullBitmap,this.valueOffsets=a.valueOffsets,this.variadicBuffers=a.variadicBuffers;else{if(this.stride=gn(t),a)(f=a[0])&&(this.valueOffsets=f),(f=a[1])&&(this.values=f),(f=a[2])&&(this.nullBitmap=f),(f=a[3])&&(this.typeIds=f);this.variadicBuffers=c}(l=this.variadicBuffers)!==null&&l!==void 0||(this.variadicBuffers=[])}getValid(t){let{type:e}=this;if(S.isUnion(e)){let n=e,r=this.typeIds[t],a=n.typeIdToChildIndex[r],o=this.children[a],s=this.valueOffsets,c=n.mode===yt.Dense&&s?Number(s[t]):t;return o.getValid(c)}if(this.nullable&&this.nullCount>0){let n=this.offset+t;return(this.nullBitmap[n>>3]&1<>3;if(!a||a.byteLength<=f)if(a=new Uint8Array((o+s+63&-64)>>3).fill(255),this.nullCount>0)a.set(Gi(o,s,this.nullBitmap),0),Object.assign(this,{nullBitmap:a});else Object.assign(this,{nullBitmap:a,_nullCount:0});let p=a[f];n=(p&l)!==0,a[f]=e?p|l:p&~l}if(n!==!!e)this._nullCount=this.nullCount+(e?-1:1);return e}clone(t=this.type,e=this.offset,n=this.length,r=this._nullCount,a=this,o=this.children,s=this.variadicBuffers){return new tt(t,e,n,r,a,o,this.dictionary,s)}slice(t,e){let{stride:n,typeId:r,children:a}=this,o=+(this._nullCount===0)-1,s=r===16?n:1,c=this._sliceBuffers(t,e,n,r);return this.clone(this.type,this.offset+t,e,o,c,a.length===0||this.valueOffsets?a:this._sliceChildren(a,s*t,s*e),this.variadicBuffers)}_changeLengthAndBackfillNullBitmap(t){if(this.typeId===y.Null)return this.clone(this.type,0,t,0,this.buffers,this.children,this.variadicBuffers);let{length:e,nullCount:n}=this,r=new Uint8Array((t+63&-64)>>3).fill(255,0,e>>3);if(r[e>>3]=(1<0)r.set(Gi(this.offset,e,this.nullBitmap),0);let a=this.buffers;return a[tn.VALIDITY]=r,this.clone(this.type,0,t,n+(t-e),a,this.children,this.variadicBuffers)}_sliceBuffers(t,e,n,r){let a,{buffers:o}=this;if((a=o[tn.TYPE])&&(o[tn.TYPE]=a.subarray(t,t+e)),S.isBinaryView(this.type)||S.isUtf8View(this.type)){let s=ct.ELEMENT_WIDTH;(a=o[tn.DATA])&&(o[tn.DATA]=a.subarray(t*s,(t+e)*s))}else(a=o[tn.OFFSET])&&(o[tn.OFFSET]=a.subarray(t,t+e+1))||(a=o[tn.DATA])&&(o[tn.DATA]=r===6?a:a.subarray(n*t,n*(t+e)));return o}_sliceChildren(t,e,n){return t.map((r)=>r.slice(e,n))}}function A(t){return Ax.visit(t)}var Sx=-1,Fm,Mo,Ax;var zn=b(()=>{je();ne();Ft();Na();Ft();ir();oe();Fm=Symbol.for("apache-arrow/Data");tt.prototype.children=Object.freeze([]);tt.prototype[Fm]=!0;Object.defineProperty(tt,Symbol.hasInstance,{value:function(e){return Function.prototype[Symbol.hasInstance].call(this,e)||this===tt&&tt.isData(e)}});Mo=class Mo extends K{visit(t){return this.getVisitFn(t.type).call(this,t)}visitNull(t){let{["type"]:e,["offset"]:n=0,["length"]:r=0}=t;return new tt(e,n,r,r)}visitBool(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length>>3,["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitInt(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length,["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitFloat(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length,["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitUtf8(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.data),a=L(t.nullBitmap),o=zi(t.valueOffsets),{["length"]:s=o.length-1,["nullCount"]:c=t.nullBitmap?-1:0}=t;return new tt(e,n,s,c,[o,r,a])}visitUtf8View(t){var e;let{["type"]:n,["offset"]:r=0}=t,a=ut(n.ArrayType,t.views),o=L(t.nullBitmap),s=(t.variadicBuffers||[]).map((f)=>L(f)),c=(e=t.length)!==null&&e!==void 0?e:Math.trunc(a.length/hn.ELEMENT_WIDTH),l=t.nullBitmap?-1:0;return new tt(n,r,c,l,[void 0,a,o],[],void 0,s)}visitLargeUtf8(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.data),a=L(t.nullBitmap),o=Ws(t.valueOffsets),{["length"]:s=o.length-1,["nullCount"]:c=t.nullBitmap?-1:0}=t;return new tt(e,n,s,c,[o,r,a])}visitBinary(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.data),a=L(t.nullBitmap),o=zi(t.valueOffsets),{["length"]:s=o.length-1,["nullCount"]:c=t.nullBitmap?-1:0}=t;return new tt(e,n,s,c,[o,r,a])}visitBinaryView(t){var e;let{["type"]:n,["offset"]:r=0}=t,a=ut(n.ArrayType,t.views),o=L(t.nullBitmap),s=(t.variadicBuffers||[]).map((f)=>L(f)),c=(e=t.length)!==null&&e!==void 0?e:Math.trunc(a.length/ct.ELEMENT_WIDTH),l=t.nullBitmap?-1:0;return new tt(n,r,c,l,[void 0,a,o],[],void 0,s)}visitLargeBinary(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.data),a=L(t.nullBitmap),o=Ws(t.valueOffsets),{["length"]:s=o.length-1,["nullCount"]:c=t.nullBitmap?-1:0}=t;return new tt(e,n,s,c,[o,r,a])}visitFixedSizeBinary(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length/gn(e),["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitDate(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length/gn(e),["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitTimestamp(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length/gn(e),["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitTime(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length/gn(e),["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitDecimal(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length/gn(e),["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitList(t){let{["type"]:e,["offset"]:n=0,["child"]:r}=t,a=L(t.nullBitmap),o=zi(t.valueOffsets),{["length"]:s=o.length-1,["nullCount"]:c=t.nullBitmap?-1:0}=t;return new tt(e,n,s,c,[o,void 0,a],[r])}visitStruct(t){let{["type"]:e,["offset"]:n=0,["children"]:r=[]}=t,a=L(t.nullBitmap),{length:o=r.reduce((c,{length:l})=>Math.max(c,l),0),nullCount:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,void 0,a],r)}visitUnion(t){let{["type"]:e,["offset"]:n=0,["children"]:r=[]}=t,a=ut(e.ArrayType,t.typeIds),{["length"]:o=a.length,["nullCount"]:s=-1}=t;if(S.isSparseUnion(e))return new tt(e,n,o,s,[void 0,void 0,void 0,a],r);let c=zi(t.valueOffsets);return new tt(e,n,o,s,[c,void 0,void 0,a],r)}visitDictionary(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.indices.ArrayType,t.data),{["dictionary"]:o=new O([new Mo().visit({type:e.dictionary})])}=t,{["length"]:s=a.length,["nullCount"]:c=t.nullBitmap?-1:0}=t;return new tt(e,n,s,c,[void 0,a,r],[],o)}visitInterval(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length/gn(e),["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitDuration(t){let{["type"]:e,["offset"]:n=0}=t,r=L(t.nullBitmap),a=ut(e.ArrayType,t.data),{["length"]:o=a.length,["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,a,r])}visitFixedSizeList(t){let{["type"]:e,["offset"]:n=0,["child"]:r=new Mo().visit({type:e.valueType})}=t,a=L(t.nullBitmap),{["length"]:o=r.length/gn(e),["nullCount"]:s=t.nullBitmap?-1:0}=t;return new tt(e,n,o,s,[void 0,void 0,a],[r])}visitMap(t){let{["type"]:e,["offset"]:n=0,["child"]:r=new Mo().visit({type:e.childType})}=t,a=L(t.nullBitmap),o=zi(t.valueOffsets),{["length"]:s=o.length-1,["nullCount"]:c=t.nullBitmap?-1:0}=t;return new tt(e,n,s,c,[o,void 0,a],[r])}};Ax=new Mo});function Um(t){return t.some((e)=>e.nullable)}function Tc(t){return t.reduce((e,n)=>e+n.nullCount,0)}function vc(t){return t.reduce((e,n,r)=>(e[r+1]=e[r]+n.length,e),new Uint32Array(t.length+1))}function Ec(t,e,n,r){let a=[];for(let o=-1,s=t.length;++o=r)break;if(n>=l+f)continue;if(l>=n&&l+f<=r){a.push(c);continue}let p=Math.max(0,n-l),_=Math.min(r-l,f);a.push(c.slice(p,_-p))}if(a.length===0)a.push(t[0].slice(0,0));return a}function Ff(t,e,n,r){let a=0,o=0,s=e.length-1;do{if(a>=s-1)return n{Ic=class Ic{constructor(t=0,e){this.numChunks=t,this.getChunkIterator=e,this.chunkIndex=0,this.chunkIterator=this.getChunkIterator(0)}next(){while(this.chunkIndex0?0:-1}function Tx(t,e){let{nullBitmap:n}=t;if(!n||t.nullCount<=0)return-1;let r=0;for(let a of new Oa(n,t.offset+(e||0),t.length,n,Ac)){if(!a)return r;++r}return-1}function J(t,e,n){if(e===void 0)return-1;if(e===null)switch(t.typeId){case y.Union:break;case y.Dictionary:break;default:return Tx(t,n)}let r=re.getVisitFn(t),a=Si(e);for(let o=(n||0)-1,s=t.length;++o{ne();ir();Yi();Na();Ra();q=class q extends K{};q.prototype.visitNull=Ix;q.prototype.visitBool=J;q.prototype.visitInt=J;q.prototype.visitInt8=J;q.prototype.visitInt16=J;q.prototype.visitInt32=J;q.prototype.visitInt64=J;q.prototype.visitUint8=J;q.prototype.visitUint16=J;q.prototype.visitUint32=J;q.prototype.visitUint64=J;q.prototype.visitFloat=J;q.prototype.visitFloat16=J;q.prototype.visitFloat32=J;q.prototype.visitFloat64=J;q.prototype.visitUtf8=J;q.prototype.visitLargeUtf8=J;q.prototype.visitUtf8View=J;q.prototype.visitBinary=J;q.prototype.visitLargeBinary=J;q.prototype.visitBinaryView=J;q.prototype.visitFixedSizeBinary=J;q.prototype.visitDate=J;q.prototype.visitDateDay=J;q.prototype.visitDateMillisecond=J;q.prototype.visitTimestamp=J;q.prototype.visitTimestampSecond=J;q.prototype.visitTimestampMillisecond=J;q.prototype.visitTimestampMicrosecond=J;q.prototype.visitTimestampNanosecond=J;q.prototype.visitTime=J;q.prototype.visitTimeSecond=J;q.prototype.visitTimeMillisecond=J;q.prototype.visitTimeMicrosecond=J;q.prototype.visitTimeNanosecond=J;q.prototype.visitDecimal=J;q.prototype.visitList=J;q.prototype.visitStruct=J;q.prototype.visitUnion=J;q.prototype.visitDenseUnion=Mm;q.prototype.visitSparseUnion=Mm;q.prototype.visitDictionary=J;q.prototype.visitInterval=J;q.prototype.visitIntervalDayTime=J;q.prototype.visitIntervalYearMonth=J;q.prototype.visitIntervalMonthDayNano=J;q.prototype.visitDuration=J;q.prototype.visitDurationSecond=J;q.prototype.visitDurationMillisecond=J;q.prototype.visitDurationMicrosecond=J;q.prototype.visitDurationNanosecond=J;q.prototype.visitFixedSizeList=J;q.prototype.visitMap=J;Qi=new q});function W(t){let{type:e}=t;if(t.nullCount===0&&t.stride===1&&(S.isInt(e)&&e.bitWidth!==64||S.isTime(e)&&e.bitWidth!==64||S.isFloat(e)&&e.precision!==Rt.HALF))return new Ic(t.data.length,(r)=>{let a=t.data[r];return a.values.subarray(0,a.length)[Symbol.iterator]()});let n=0;return new Ic(t.data.length,(r)=>{let o=t.data[r].length,s=t.slice(n,n+o);return n+=o,new Lm(s)})}var z,Lm,Ua;var Oc=b(()=>{ir();ne();Ft();Cc();z=class z extends K{};Lm=class Lm{constructor(t){this.vector=t,this.index=0}next(){if(this.indexvx(e)));if(ArrayBuffer.isView(t)){if(t instanceof DataView)t=new Uint8Array(t.buffer);let e={offset:0,length:t.length,nullCount:-1,data:t};if(t instanceof Int8Array)return new O([A(Object.assign(Object.assign({},e),{type:new kr}))]);if(t instanceof Int16Array)return new O([A(Object.assign(Object.assign({},e),{type:new Vr}))]);if(t instanceof Int32Array)return new O([A(Object.assign(Object.assign({},e),{type:new Ee}))]);if(t instanceof BigInt64Array)return new O([A(Object.assign(Object.assign({},e),{type:new De}))]);if(t instanceof Uint8Array||t instanceof Uint8ClampedArray)return new O([A(Object.assign(Object.assign({},e),{type:new qr}))]);if(t instanceof Uint16Array)return new O([A(Object.assign(Object.assign({},e),{type:new zr}))]);if(t instanceof Uint32Array)return new O([A(Object.assign(Object.assign({},e),{type:new jr}))]);if(t instanceof BigUint64Array)return new O([A(Object.assign(Object.assign({},e),{type:new qn}))]);if(t instanceof Float32Array)return new O([A(Object.assign(Object.assign({},e),{type:new $r}))]);if(t instanceof Float64Array)return new O([A(Object.assign(Object.assign({},e),{type:new _n}))]);throw Error("Unrecognized input")}}throw Error("Unrecognized input")}function vx(t){return t instanceof tt?[t]:t instanceof O?t.data:xr(t).data}var Pm,km,Vm,qm,O,Nc;var je=b(()=>{ne();Ra();Ft();zn();Cc();Yi();bn();Rc();Oc();Ft();km=Symbol.for("apache-arrow/Vector"),Vm={},qm={};O=class O{static isVector(t){return(t===null||t===void 0?void 0:t[km])===!0}constructor(t){var e,n,r;let a=t[0]instanceof O?t.flatMap((s)=>s.data):t;if(a.length===0||a.some((s)=>!(s instanceof tt)))throw TypeError("Vector constructor expects an Array of Data instances.");let o=(e=a[0])===null||e===void 0?void 0:e.type;switch(a.length){case 0:this._offsets=[0];break;case 1:{let{get:s,set:c,indexOf:l}=Vm[o.typeId],f=a[0];this.isValid=(p)=>Lo(f,p),this.get=(p)=>s(f,p),this.set=(p,_)=>c(f,p,_),this.indexOf=(p)=>l(f,p),this._offsets=[0,f.length];break}default:Object.setPrototypeOf(this,qm[o.typeId]),this._offsets=vc(a);break}this.data=a,this.type=o,this.stride=gn(o),this.numChildren=(r=(n=o.children)===null||n===void 0?void 0:n.length)!==null&&r!==void 0?r:0,this.length=this._offsets.at(-1)}get byteLength(){return this.data.reduce((t,e)=>t+e.byteLength,0)}get nullable(){return Um(this.data)}get nullCount(){return Tc(this.data)}get ArrayType(){return this.type.ArrayType}get[Symbol.toStringTag](){return`${this.VectorName}<${this.type[Symbol.toStringTag]}>`}get VectorName(){return`${y[this.type.typeId]}Vector`}isValid(t){return!1}get(t){return null}at(t){return this.get(Ki(t,this.length))}set(t,e){return}indexOf(t,e){return-1}includes(t,e){return this.indexOf(t,e)>-1}[Symbol.iterator](){return Ua.visit(this)}concat(...t){return new O(this.data.concat(t.flatMap((e)=>e.data).flat(Number.POSITIVE_INFINITY)))}slice(t,e){return new O(Fo(this,t,e,({data:n,_offsets:r},a,o)=>Ec(n,r,a,o)))}toJSON(){return[...this]}toArray(){let{type:t,data:e,length:n,stride:r,ArrayType:a}=this;switch(t.typeId){case y.Int:case y.Float:case y.Decimal:case y.Time:case y.Timestamp:switch(e.length){case 0:return new a;case 1:return e[0].values.subarray(0,n*r);default:return e.reduce((o,{values:s,length:c})=>(o.array.set(s.subarray(0,c*r),o.offset),o.offset+=c*r,o),{array:new a(n*r),offset:0}).array}}return[...this]}toString(){return`[${[...this].join(",")}]`}getChild(t){var e;return this.getChildAt((e=this.type.children)===null||e===void 0?void 0:e.findIndex((n)=>n.name===t))}getChildAt(t){if(t>-1&&te[t]));return null}get isMemoized(){if(S.isDictionary(this.type))return this.data[0].dictionary.isMemoized;return!1}memoize(){if(S.isDictionary(this.type)){let t=new Nc(this.data[0].dictionary),e=this.data.map((n)=>{let r=n.clone();return r.dictionary=t,r});return new O(e)}return new Nc(this)}unmemoize(){if(S.isDictionary(this.type)&&this.isMemoized){let t=this.data[0].dictionary.unmemoize(),e=this.data.map((n)=>{let r=n.clone();return r.dictionary=t,r});return new O(e)}return this}};Pm=Symbol.toStringTag;O[Pm]=((t)=>{t.type=S.prototype,t.data=[],t.length=0,t.stride=1,t.numChildren=0,t._offsets=new Uint32Array([0]),t[Symbol.isConcatSpreadable]=!0,t[km]=!0;let e=Object.keys(y).map((n)=>y[n]).filter((n)=>typeof n==="number"&&n!==y.NONE);for(let n of e){let r=re.getVisitFnByTypeId(n),a=Be.getVisitFnByTypeId(n),o=Qi.getVisitFnByTypeId(n);Vm[n]={get:r,set:a,indexOf:o},qm[n]=Object.create(t,{["isValid"]:{value:Fa(Lo)},["get"]:{value:Fa(re.getVisitFnByTypeId(n))},["set"]:{value:Dc(Be.getVisitFnByTypeId(n))},["indexOf"]:{value:Bc(Qi.getVisitFnByTypeId(n))}})}return"Vector"})(O.prototype);Object.defineProperty(O,Symbol.hasInstance,{value:function(e){return Function.prototype[Symbol.hasInstance].call(this,e)||this===O&&O.isVector(e)}});Nc=class Nc extends O{constructor(t){super(t.data);let e=this.get,n=this.set,r=this.slice,a=Array(this.length);Object.defineProperty(this,"get",{value(o){let s=a[o];if(s!==void 0)return s;let c=e.call(this,o);return a[o]=c,c}}),Object.defineProperty(this,"set",{value(o,s){n.call(this,o,s),a[o]=s}}),Object.defineProperty(this,"slice",{value:(o,s)=>new Nc(r.call(this,o,s))}),Object.defineProperty(this,"isMemoized",{value:!0}),Object.defineProperty(this,"unmemoize",{value:()=>new O(this.data)}),Object.defineProperty(this,"memoize",{value:()=>this})}}});function zm(t){if(!t||t.length<=0)return function(a){return!0};let e="",n=t.filter((r)=>r===r);if(n.length>0)e=` + switch (x) {${n.map((r)=>` + case ${Ex(r)}:`).join("")} return false; - }`;if($.length!==Q.length)Z=`if (x !== x) return false; -${Z}`;return Function("x",`${Z} -return true;`)}function rO($){if(typeof $!=="bigint")return A8($);return`${A8($)}n`}var qP=()=>{};function Y2($,Z){let Q=Math.ceil($)*Z-1;return(Q-Q%64+64||64)/Z}function PP($,Z=0){return $.length>=Z?$.subarray(0,Z):FY(new $.constructor(Z),$,0)}class wQ{constructor($,Z=0,Q=1){this.length=Math.ceil(Z/Q),this.buffer=new $(this.length),this.stride=Q,this.BYTES_PER_ELEMENT=$.BYTES_PER_ELEMENT,this.ArrayType=$}get byteLength(){return Math.ceil(this.length*this.stride)*this.BYTES_PER_ELEMENT}get reservedLength(){return this.buffer.length/this.stride}get reservedByteLength(){return this.buffer.byteLength}set($,Z){return this}append($){return this.set(this.length,$)}reserve($){if($>0){this.length+=$;let Z=this.stride,Q=this.length*Z,X=this.buffer.length;if(Q>=X)this._resize(X===0?Y2(Q*1,this.BYTES_PER_ELEMENT):Y2(Q*2,this.BYTES_PER_ELEMENT))}return this}flush($=this.length){$=Y2($*this.stride,this.BYTES_PER_ELEMENT);let Z=PP(this.buffer,$);return this.clear(),Z}clear(){return this.length=0,this.buffer=new this.ArrayType,this}_resize($){return this.buffer=PP(this.buffer,$)}}var FJ,TY,gY;var $X=O(()=>{s$();FJ=class FJ extends wQ{last(){return this.get(this.length-1)}get($){return this.buffer[$]}set($,Z){return this.reserve($-this.length+1),this.buffer[$*this.stride]=Z,this}};TY=class TY extends FJ{constructor(){super(Uint8Array,0,0.125);this.numValid=0}get numInvalid(){return this.length-this.numValid}get($){return this.buffer[$>>3]>>$%8&1}set($,Z){let{buffer:Q}=this.reserve($-this.length+1),X=$>>3,J=$%8,W=Q[X]>>J&1;return Z?W===0&&(Q[X]|=1<=0)X.fill(X[Q],Q,$);return X[$]=X[$-1]+Z,this}flush($=this.length-1){if($>this.length)this.set($-1,this.BYTES_PER_ELEMENT>4?BigInt(0):0);return super.flush($+1)}}});class M${static throughNode($){throw Error('"throughNode" not available in this environment')}static throughDOM($){throw Error('"throughDOM" not available in this environment')}constructor({type:$,nullValues:Z}){if(this.length=0,this.finished=!1,this.type=$,this.children=[],this.nullValues=Z,this.stride=RQ($),this._nulls=new TY,Z&&Z.length>0)this._isValid=zP(Z)}toVector(){return new r([this.flush()])}get ArrayType(){return this.type.ArrayType}get nullCount(){return this._nulls.numInvalid}get numChildren(){return this.children.length}get byteLength(){let $=0,{_offsets:Z,_values:Q,_nulls:X,_typeIds:J,children:W}=this;return Z&&($+=Z.byteLength),Q&&($+=Q.byteLength),X&&($+=X.byteLength),J&&($+=J.byteLength),W.reduce((Y,G)=>Y+G.byteLength,$)}get reservedLength(){return this._nulls.reservedLength}get reservedByteLength(){let $=0;return this._offsets&&($+=this._offsets.reservedByteLength),this._values&&($+=this._values.reservedByteLength),this._nulls&&($+=this._nulls.reservedByteLength),this._typeIds&&($+=this._typeIds.reservedByteLength),this.children.reduce((Z,Q)=>Z+Q.reservedByteLength,$)}get valueOffsets(){return this._offsets?this._offsets.buffer:null}get values(){return this._values?this._values.buffer:null}get nullBitmap(){return this._nulls?this._nulls.buffer:null}get typeIds(){return this._typeIds?this._typeIds.buffer:null}append($){return this.set(this.length,$)}isValid($){return this._isValid($)}set($,Z){if(this.setValid($,this.isValid(Z)))this.setValue($,Z);return this}setValue($,Z){this._setValue(this,$,Z)}setValid($,Z){return this.length=this._nulls.set($,+Z).length,Z}addChild($,Z=`${this.numChildren}`){throw Error(`Cannot append children to non-nested type "${this.type}"`)}getChildAt($){return this.children[$]||null}flush(){let $,Z,Q,X,{type:J,length:W,nullCount:Y,_typeIds:G,_offsets:H,_values:z,_nulls:q}=this;if(Z=G===null||G===void 0?void 0:G.flush(W))X=H===null||H===void 0?void 0:H.flush(W);else if(X=H===null||H===void 0?void 0:H.flush(W))$=z===null||z===void 0?void 0:z.flush(H.last());else $=z===null||z===void 0?void 0:z.flush(W);if(Y>0)Q=q===null||q===void 0?void 0:q.flush(W);let P=this.children.map((U)=>U.flush());return this.clear(),I({type:J,length:W,nullCount:Y,children:P,child:P[0],data:$,typeIds:Z,nullBitmap:Q,valueOffsets:X})}finish(){this.finished=!0;for(let $ of this.children)$.finish();return this}clear(){var $,Z,Q,X;this.length=0,($=this._nulls)===null||$===void 0||$.clear(),(Z=this._values)===null||Z===void 0||Z.clear(),(Q=this._offsets)===null||Q===void 0||Q.clear(),(X=this._typeIds)===null||X===void 0||X.clear();for(let J of this.children)J.clear();return this}}var r$,SQ;var R$=O(()=>{ZQ();$8();B4();H$();qP();$X();M$.prototype.length=1;M$.prototype.stride=1;M$.prototype.children=null;M$.prototype.finished=!1;M$.prototype.nullValues=null;M$.prototype._isValid=()=>!0;r$=class r$ extends M${constructor($){super($);this._values=new FJ(this.ArrayType,0,this.stride)}setValue($,Z){let Q=this._values;return Q.reserve($-Q.length+1),super.setValue($,Z)}};SQ=class SQ extends M${constructor($){super($);this._pendingLength=0,this._offsets=new gY($.type)}setValue($,Z){let Q=this._pending||(this._pending=new Map),X=Q.get($);X&&(this._pendingLength-=X.length),this._pendingLength+=Z instanceof RX?Z[qJ].length:Z.length,Q.set($,Z)}setValid($,Z){if(!super.setValid($,Z))return(this._pending||(this._pending=new Map)).set($,void 0),!1;return!0}clear(){return this._pendingLength=0,this._pending=void 0,super.clear()}flush(){return this._flush(),super.flush()}finish(){return this._flush(),super.finish()}_flush(){let $=this._pending,Z=this._pendingLength;if(this._pendingLength=0,this._pending=void 0,$&&$.size>0)this._flushPending($,Z);return this}}});class zW{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}offset(){return this.bb.readInt64(this.bb_pos)}metaDataLength(){return this.bb.readInt32(this.bb_pos+8)}bodyLength(){return this.bb.readInt64(this.bb_pos+16)}static sizeOf(){return 24}static createBlock($,Z,Q,X){return $.prep(8,24),$.writeInt64(BigInt(X!==null&&X!==void 0?X:0)),$.pad(4),$.writeInt32(Q),$.writeInt64(BigInt(Z!==null&&Z!==void 0?Z:0)),$.offset()}}class QQ{constructor(){this.bb=null,this.bb_pos=0}__init($,Z){return this.bb_pos=$,this.bb=Z,this}static getRootAsFooter($,Z){return(Z||new QQ).__init($.readInt32($.position())+$.position(),$)}static getSizePrefixedRootAsFooter($,Z){return $.setPosition($.position()+t),(Z||new QQ).__init($.readInt32($.position())+$.position(),$)}version(){let $=this.bb.__offset(this.bb_pos,4);return $?this.bb.readInt16(this.bb_pos+$):i0.V1}schema($){let Z=this.bb.__offset(this.bb_pos,6);return Z?($||new _$).__init(this.bb.__indirect(this.bb_pos+Z),this.bb):null}dictionaries($,Z){let Q=this.bb.__offset(this.bb_pos,8);return Q?(Z||new zW).__init(this.bb.__vector(this.bb_pos+Q)+$*24,this.bb):null}dictionariesLength(){let $=this.bb.__offset(this.bb_pos,8);return $?this.bb.__vector_len(this.bb_pos+$):0}recordBatches($,Z){let Q=this.bb.__offset(this.bb_pos,10);return Q?(Z||new zW).__init(this.bb.__vector(this.bb_pos+Q)+$*24,this.bb):null}recordBatchesLength(){let $=this.bb.__offset(this.bb_pos,10);return $?this.bb.__vector_len(this.bb_pos+$):0}customMetadata($,Z){let Q=this.bb.__offset(this.bb_pos,12);return Q?(Z||new K$).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+Q)+$*4),this.bb):null}customMetadataLength(){let $=this.bb.__offset(this.bb_pos,12);return $?this.bb.__vector_len(this.bb_pos+$):0}static startFooter($){$.startObject(5)}static addVersion($,Z){$.addFieldInt16(0,Z,i0.V1)}static addSchema($,Z){$.addFieldOffset(1,Z,0)}static addDictionaries($,Z){$.addFieldOffset(2,Z,0)}static startDictionariesVector($,Z){$.startVector(24,Z,8)}static addRecordBatches($,Z){$.addFieldOffset(3,Z,0)}static startRecordBatchesVector($,Z){$.startVector(24,Z,8)}static addCustomMetadata($,Z){$.addFieldOffset(4,Z,0)}static createCustomMetadataVector($,Z){$.startVector(4,Z.length,4);for(let Q=Z.length-1;Q>=0;Q--)$.addOffset(Z[Q]);return $.endVector()}static startCustomMetadataVector($,Z){$.startVector(4,Z,4)}static endFooter($){return $.endObject()}static finishFooterBuffer($,Z){$.finish(Z)}static finishSizePrefixedFooterBuffer($,Z){$.finish(Z,void 0,!0)}}var FP=O(()=>{y0();s6();Y4();bK()});function p4($,Z){return new Map([...$||new Map,...Z||new Map])}function G2($,Z=new Map){for(let Q=-1,X=$.length;++Q0)G2(W.children,Z)}return Z}var NP,UP,Q0,a;var vZ=O(()=>{u$();H$();NP=Symbol.for("apache-arrow/Schema"),UP=Symbol.for("apache-arrow/Field");Q0=class Q0{static isSchema($){return($===null||$===void 0?void 0:$[NP])===!0}constructor($=[],Z,Q,X=i0.V5){if(this.fields=$||[],this.metadata=Z||new Map,!Q)Q=G2(this.fields);this.dictionaries=Q,this.metadataVersion=X}get[Symbol.toStringTag](){return"Schema"}get names(){return this.fields.map(($)=>$.name)}toString(){return`Schema<{ ${this.fields.map(($,Z)=>`${Z}: ${$}`).join(", ")} }>`}select($){let Z=new Set($),Q=this.fields.filter((X)=>Z.has(X.name));return new Q0(Q,this.metadata)}selectAt($){let Z=$.map((Q)=>this.fields[Q]).filter(Boolean);return new Q0(Z,this.metadata)}assign(...$){let Z=$[0]instanceof Q0?$[0]:Array.isArray($[0])?new Q0($[0]):new Q0($),Q=[...this.fields],X=p4(p4(new Map,this.metadata),Z.metadata),J=Z.fields.filter((Y)=>{let G=Q.findIndex((H)=>H.name===Y.name);return~G?(Q[G]=Y.clone({metadata:p4(p4(new Map,Q[G].metadata),Y.metadata)}))&&!1:!0}),W=G2(J,new Map);return new Q0([...Q,...J],X,new Map([...this.dictionaries,...W]))}};Q0.prototype.fields=null;Q0.prototype.metadata=null;Q0.prototype.dictionaries=null;Q0.prototype[NP]=!0;Object.defineProperty(Q0,Symbol.hasInstance,{value:function(Z){return Function.prototype[Symbol.hasInstance].call(this,Z)||this===Q0&&Q0.isSchema(Z)}});a=class a{static isField($){return($===null||$===void 0?void 0:$[UP])===!0}static new(...$){let[Z,Q,X,J]=$;if($[0]&&typeof $[0]==="object")({name:Z}=$[0]),Q===void 0&&(Q=$[0].type),X===void 0&&(X=$[0].nullable),J===void 0&&(J=$[0].metadata);return new a(`${Z}`,Q,X,J)}constructor($,Z,Q=!1,X){this.name=$,this.type=Z,this.nullable=Q,this.metadata=X||new Map}get typeId(){return this.type.typeId}get[Symbol.toStringTag](){return"Field"}toString(){return`${this.name}: ${this.type}`}clone(...$){let[Z,Q,X,J]=$;return!$[0]||typeof $[0]!=="object"?[Z=this.name,Q=this.type,X=this.nullable,J=this.metadata]=$:{name:Z=this.name,type:Q=this.type,nullable:X=this.nullable,metadata:J=this.metadata}=$[0],a.new(Z,Q,X,J)}};a.prototype.type=null;a.prototype.name=null;a.prototype.nullable=null;a.prototype.metadata=null;a.prototype[UP]=!0;Object.defineProperty(a,Symbol.hasInstance,{value:function(Z){return Function.prototype[Symbol.hasInstance].call(this,Z)||this===a&&a.isField(Z)}})});class NJ{static decode($){$=new tO($0($));let Z=QQ.getRootAsFooter($),Q=Q0.decode(Z.schema(),new Map,Z.version());return new MP(Q,Z)}static encode($){let Z=new aO,Q=Q0.encode(Z,$.schema);QQ.startRecordBatchesVector(Z,$.numRecordBatches);for(let W of[...$.recordBatches()].slice().reverse())EX.encode(Z,W);let X=Z.endVector();QQ.startDictionariesVector(Z,$.numDictionaries);for(let W of[...$.dictionaryBatches()].slice().reverse())EX.encode(Z,W);let J=Z.endVector();return QQ.startFooter(Z),QQ.addSchema(Z,Q),QQ.addVersion(Z,i0.V5),QQ.addRecordBatches(Z,X),QQ.addDictionaries(Z,J),QQ.finishFooterBuffer(Z,QQ.endFooter(Z)),Z.asUint8Array()}get numRecordBatches(){return this._recordBatches.length}get numDictionaries(){return this._dictionaryBatches.length}constructor($,Z=i0.V5,Q,X){this.schema=$,this.version=Z,Q&&(this._recordBatches=Q),X&&(this._dictionaryBatches=X)}*recordBatches(){for(let $,Z=-1,Q=this.numRecordBatches;++Z=0&&$=0&&${FP();y0();vZ();u$();s$();aO=sJ,tO=UZ;MP=class MP extends NJ{get numRecordBatches(){return this._footer.recordBatchesLength()}get numDictionaries(){return this._footer.dictionariesLength()}constructor($,Z){super($,Z.version());this._footer=Z}getRecordBatch($){if($>=0&&$=0&&$=0;Q--)$.addOffset(Z[Q]);return $.endVector()}static startCustomMetadataVector($,Z){$.startVector(4,Z,4)}static endMessage($){return $.endObject()}static finishMessageBuffer($,Z){$.finish(Z)}static finishSizePrefixedMessageBuffer($,Z){$.finish(Z,void 0,!0)}static createMessage($,Z,Q,X,J,W){return g$.startMessage($),g$.addVersion($,Z),g$.addHeaderType($,Q),g$.addHeader($,X),g$.addBodyLength($,J),g$.addCustomMetadata($,W),g$.endMessage($)}}var LP=O(()=>{y0();s6();mK();Y4()});var AP,c4;var DP=O(()=>{R8();y9();H4();IK();I9();_9();g9();T9();b9();m9();x9();wK();EK();gK();xK();_K();SK();v9();h9();vK();RK();BK();kK();TK();AP=class AP extends H0{visit($,Z){return $==null||Z==null?void 0:super.visit($,Z)}visitNull($,Z){return WJ.startNull(Z),WJ.endNull(Z)}visitInt($,Z){return RZ.startInt(Z),RZ.addBitWidth(Z,$.bitWidth),RZ.addIsSigned(Z,$.isSigned),RZ.endInt(Z)}visitFloat($,Z){return nQ.startFloatingPoint(Z),nQ.addPrecision(Z,$.precision),nQ.endFloatingPoint(Z)}visitBinary($,Z){return eX.startBinary(Z),eX.endBinary(Z)}visitBinaryView($,Z){return $J.startBinaryView(Z),$J.endBinaryView(Z)}visitLargeBinary($,Z){return QJ.startLargeBinary(Z),QJ.endLargeBinary(Z)}visitBool($,Z){return ZJ.startBool(Z),ZJ.endBool(Z)}visitUtf8($,Z){return GJ.startUtf8(Z),GJ.endUtf8(Z)}visitUtf8View($,Z){return KJ.startUtf8View(Z),KJ.endUtf8View(Z)}visitLargeUtf8($,Z){return XJ.startLargeUtf8(Z),XJ.endLargeUtf8(Z)}visitDecimal($,Z){return EZ.startDecimal(Z),EZ.addScale(Z,$.scale),EZ.addPrecision(Z,$.precision),EZ.addBitWidth(Z,$.bitWidth),EZ.endDecimal(Z)}visitDate($,Z){return pQ.startDate(Z),pQ.addUnit(Z,$.unit),pQ.endDate(Z)}visitTime($,Z){return iZ.startTime(Z),iZ.addUnit(Z,$.unit),iZ.addBitWidth(Z,$.bitWidth),iZ.endTime(Z)}visitTimestamp($,Z){let Q=$.timezone&&Z.createString($.timezone)||void 0;if(rZ.startTimestamp(Z),rZ.addUnit(Z,$.unit),Q!==void 0)rZ.addTimezone(Z,Q);return rZ.endTimestamp(Z)}visitInterval($,Z){return oQ.startInterval(Z),oQ.addUnit(Z,$.unit),oQ.endInterval(Z)}visitDuration($,Z){return cQ.startDuration(Z),cQ.addUnit(Z,$.unit),cQ.endDuration(Z)}visitList($,Z){return JJ.startList(Z),JJ.endList(Z)}visitStruct($,Z){return YJ.startStruct_(Z),YJ.endStruct_(Z)}visitUnion($,Z){wZ.startTypeIdsVector(Z,$.typeIds.length);let Q=wZ.createTypeIdsVector(Z,$.typeIds);return wZ.startUnion(Z),wZ.addMode(Z,$.mode),wZ.addTypeIds(Z,Q),wZ.endUnion(Z)}visitDictionary($,Z){let Q=this.visit($.indices,Z);if(L8.startDictionaryEncoding(Z),L8.addId(Z,BigInt($.id)),L8.addIsOrdered(Z,$.isOrdered),Q!==void 0)L8.addIndexType(Z,Q);return L8.endDictionaryEncoding(Z)}visitFixedSizeBinary($,Z){return lQ.startFixedSizeBinary(Z),lQ.addByteWidth(Z,$.byteWidth),lQ.endFixedSizeBinary(Z)}visitFixedSizeList($,Z){return dQ.startFixedSizeList(Z),dQ.addListSize(Z,$.listSize),dQ.endFixedSizeList(Z)}visitMap($,Z){return sQ.startMap(Z),sQ.addKeysSorted(Z,$.keysSorted),sQ.endMap(Z)}};c4=new AP});function VP($,Z=new Map){return new Q0(eO($,Z),l4($.metadata),Z)}function H2($){return new a$($.count,EP($.columns),wP($.columns),null,SP($.columns))}function RP($){return new yZ(H2($.data),$.id,$.isDelta)}function eO($,Z){return($.fields||[]).filter(Boolean).map((Q)=>a.fromJSON(Q,Z))}function OP($,Z){return($.children||[]).filter(Boolean).map((Q)=>a.fromJSON(Q,Z))}function EP($){return($||[]).reduce((Z,Q)=>[...Z,new w8(Q.count,$C(Q.VALIDITY)),...EP(Q.children)],[])}function wP($,Z=[]){for(let Q=-1,X=($||[]).length;++QZ+ +(Q===0),0)}function SP($){return($||[]).reduce((Z,Q)=>[...Z,...Q.VARIADIC_DATA_BUFFERS?[Q.VARIADIC_DATA_BUFFERS.length]:[],...SP(Q.children)],[])}function BP($,Z){let Q,X,J,W,Y,G;if(!Z||!(W=$.dictionary))Y=jP($,OP($,Z)),J=new a($.name,Y,$.nullable,l4($.metadata));else if(!Z.has(Q=W.id))X=(X=W.indexType)?CP(X):new GZ,Z.set(Q,Y=jP($,OP($,Z))),G=new YZ(Y,X,Q,W.isOrdered),J=new a($.name,G,$.nullable,l4($.metadata));else X=(X=W.indexType)?CP(X):new GZ,G=new YZ(Z.get(Q),X,Q,W.isOrdered),J=new a($.name,G,$.nullable,l4($.metadata));return J||null}function l4($=[]){return new Map($.map(({key:Z,value:Q})=>[Z,Q]))}function CP($){return new i$($.isSigned,$.bitWidth)}function jP($,Z){let Q=$.type.name;switch(Q){case"NONE":return new j$;case"null":return new j$;case"binary":return new xZ;case"largebinary":return new rQ;case"binaryview":return new R0;case"utf8":return new T$;case"largeutf8":return new aQ;case"utf8view":return new OQ;case"bool":return new V$;case"list":return new LZ((Z||[])[0]);case"struct":return new j0(Z||[]);case"struct_":return new j0(Z||[])}switch(Q){case"int":{let X=$.type;return new i$(X.isSigned,X.bitWidth)}case"floatingpoint":{let X=$.type;return new iQ(J$[X.precision])}case"decimal":{let X=$.type;return new tQ(X.scale,X.precision,X.bitWidth)}case"date":{let X=$.type;return new O8(r0[X.unit])}case"time":{let X=$.type;return new C8(l[X.unit],X.bitWidth)}case"timestamp":{let X=$.type;return new tZ(l[X.unit],X.timezone)}case"interval":{let X=$.type;return new j8(s0[X.unit])}case"duration":{let X=$.type;return new V8(l[X.unit])}case"union":{let X=$.type,[J,...W]=(X.mode+"").toLowerCase(),Y=J.toUpperCase()+W.join("");return new CQ(m0[Y],X.typeIds||[],Z||[])}case"fixedsizebinary":{let X=$.type;return new eQ(X.byteWidth)}case"fixedsizelist":{let X=$.type;return new jQ(X.listSize,(Z||[])[0])}case"map":{let X=$.type;return new eZ((Z||[])[0],X.keysSorted)}}throw Error(`Unrecognized type: "${Q}"`)}var kP=O(()=>{vZ();H$();X6();u$()});class KZ{static fromJSON($,Z){let Q=new KZ(0,i0.V5,Z);return Q._createHeader=XC($,Z),Q}static decode($){$=new QC($0($));let Z=g$.getRootAsMessage($),Q=Z.bodyLength(),X=Z.version(),J=Z.headerType(),W=UC(Z),Y=new KZ(Q,X,J,void 0,W);return Y._createHeader=JC(Z,J),Y}static encode($){let Z=new ZC,Q=-1;if($.isSchema())Q=Q0.encode(Z,$.header());else if($.isRecordBatch())Q=a$.encode(Z,$.header());else if($.isDictionaryBatch())Q=yZ.encode(Z,$.header());let X=!($.metadata&&$.metadata.size>0)?-1:g$.createCustomMetadataVector(Z,[...$.metadata].map(([J,W])=>{let Y=Z.createString(`${J}`),G=Z.createString(`${W}`);return K$.startKeyValue(Z),K$.addKey(Z,Y),K$.addValue(Z,G),K$.endKeyValue(Z)}));if(g$.startMessage(Z),g$.addVersion(Z,i0.V5),g$.addHeader(Z,Q),g$.addHeaderType(Z,$.headerType),g$.addBodyLength(Z,BigInt($.bodyLength)),X!==-1)g$.addCustomMetadata(Z,X);return g$.finishMessageBuffer(Z,g$.endMessage(Z)),Z.asUint8Array()}static from($,Z=0){if($ instanceof Q0)return new KZ(0,i0.V5,T0.Schema,$);if($ instanceof a$)return new KZ(Z,i0.V5,T0.RecordBatch,$,$.metadata);if($ instanceof yZ)return new KZ(Z,i0.V5,T0.DictionaryBatch,$);throw Error(`Unrecognized Message header: ${$}`)}get type(){return this.headerType}get version(){return this._version}get headerType(){return this._headerType}get compression(){return this._compression}get bodyLength(){return this._bodyLength}get metadata(){return this._metadata}header(){return this._createHeader()}isSchema(){return this.headerType===T0.Schema}isRecordBatch(){return this.headerType===T0.RecordBatch}isDictionaryBatch(){return this.headerType===T0.DictionaryBatch}constructor($,Z,Q,X,J){this._version=Z,this._headerType=Q,this.body=new Uint8Array(0),this._compression=X===null||X===void 0?void 0:X.compression,X&&(this._createHeader=()=>X),this._bodyLength=c0($),this._metadata=J||new Map}}class a${get nodes(){return this._nodes}get length(){return this._length}get buffers(){return this._buffers}get compression(){return this._compression}get variadicBufferCounts(){return this._variadicBufferCounts}get metadata(){return this._metadata}constructor($,Z,Q,X,J=[],W){this._nodes=Z,this._buffers=Q,this._length=c0($),this._compression=X,this._variadicBufferCounts=J,this._metadata=W||new Map}}class yZ{get id(){return this._id}get data(){return this._data}get isDelta(){return this._isDelta}get length(){return this.data.length}get nodes(){return this.data.nodes}get buffers(){return this.data.buffers}constructor($,Z,Q=!1){this._data=$,this._isDelta=Q,this._id=c0(Z)}}class x${constructor($,Z){this.offset=c0($),this.length=c0(Z)}}class w8{constructor($,Z){this.length=c0($),this.nullCount=c0(Z)}}class qW{constructor($,Z=iJ.BUFFER){this.type=$,this.method=Z}}function XC($,Z){return()=>{switch(Z){case T0.Schema:return Q0.fromJSON($);case T0.RecordBatch:return a$.fromJSON($);case T0.DictionaryBatch:return yZ.fromJSON($)}throw Error(`Unrecognized Message type: { name: ${T0[Z]}, type: ${Z} }`)}}function JC($,Z){return()=>{switch(Z){case T0.Schema:return Q0.decode($.header(new _$),new Map,$.version());case T0.RecordBatch:return a$.decode($.header(new MZ),$.version());case T0.DictionaryBatch:return yZ.decode($.header(new o8),$.version())}throw Error(`Unrecognized Message type: { name: ${T0[Z]}, type: ${Z} }`)}}function WC($,Z=new Map,Q=i0.V5){let X=FC($,Z);return new Q0(X,d4($),Z,Q)}function YC($,Z=i0.V5){return new a$($.length(),zC($),qC($,Z),gP($.compression()),PC($))}function GC($,Z=i0.V5){return new yZ(a$.decode($.data(),Z),$.id(),$.isDelta())}function KC($){return new x$($.offset(),$.length())}function HC($){return new w8($.length(),$.nullCount())}function zC($){let Z=[];for(let Q,X=-1,J=-1,W=$.nodesLength();++Xa.encode($,W));_$.startFieldsVector($,Q.length);let X=_$.createFieldsVector($,Q),J=!(Z.metadata&&Z.metadata.size>0)?-1:_$.createCustomMetadataVector($,[...Z.metadata].map(([W,Y])=>{let G=$.createString(`${W}`),H=$.createString(`${Y}`);return K$.startKeyValue($),K$.addKey($,G),K$.addValue($,H),K$.endKeyValue($)}));if(_$.startSchema($),_$.addFields($,X),_$.addEndianness($,jC?tX.Little:tX.Big),J!==-1)_$.addCustomMetadata($,J);return _$.endSchema($)}function LC($,Z){let Q=-1,X=-1,J=-1,W=Z.type,Y=Z.typeId;if(!k.isDictionary(W))X=c4.visit(W,$);else Y=W.dictionary.typeId,J=c4.visit(W,$),X=c4.visit(W.dictionary,$);let G=(W.children||[]).map((q)=>a.encode($,q)),H=WZ.createChildrenVector($,G),z=!(Z.metadata&&Z.metadata.size>0)?-1:WZ.createCustomMetadataVector($,[...Z.metadata].map(([q,P])=>{let U=$.createString(`${q}`),M=$.createString(`${P}`);return K$.startKeyValue($),K$.addKey($,U),K$.addValue($,M),K$.endKeyValue($)}));if(Z.name)Q=$.createString(Z.name);if(WZ.startField($),WZ.addType($,X),WZ.addTypeType($,Y),WZ.addChildren($,H),WZ.addNullable($,!!Z.nullable),Q!==-1)WZ.addName($,Q);if(J!==-1)WZ.addDictionary($,J);if(z!==-1)WZ.addCustomMetadata($,z);return WZ.endField($)}function AC($,Z){let Q=Z.nodes||[],X=Z.buffers||[],J=Z.variadicBufferCounts||[];MZ.startNodesVector($,Q.length);for(let z of Q.slice().reverse())w8.encode($,z);let W=$.endVector();MZ.startBuffersVector($,X.length);for(let z of X.slice().reverse())x$.encode($,z);let Y=$.endVector(),G=null;if(Z.compression!==null)G=xP($,Z.compression);let H=-1;if(J.length>0)H=MZ.createVariadicBufferCountsVector($,J.map(BigInt));if(MZ.startRecordBatch($),MZ.addLength($,BigInt(Z.length)),MZ.addNodes($,W),MZ.addBuffers($,Y),Z.compression!==null&&G)MZ.addCompression($,G);if(H!==-1)MZ.addVariadicBufferCounts($,H);return MZ.endRecordBatch($)}function xP($,Z){return sZ.startBodyCompression($),sZ.addCodec($,Z.type),sZ.addMethod($,Z.method),sZ.endBodyCompression($)}function DC($,Z){let Q=a$.encode($,Z.data);return o8.startDictionaryBatch($),o8.addId($,BigInt(Z.id)),o8.addIsDelta($,Z.isDelta),o8.addData($,Q),o8.endDictionaryBatch($)}function OC($,Z){return OY.createFieldNode($,BigInt(Z.length),BigInt(Z.nullCount))}function CC($,Z){return DY.createBuffer($,BigInt(Z.offset),BigInt(Z.length))}var ZC,QC,jC;var X6=O(()=>{y0();bK();H4();jK();B9();hK();yK();s6();VK();IK();wK();EK();gK();xK();_K();SK();vK();BK();kK();TK();LP();CK();OK();vZ();s$();u$();DP();kP();H$();ZC=sJ,QC=UZ;a.encode=LC;a.decode=NC;a.fromJSON=BP;Q0.encode=MC;Q0.decode=WC;Q0.fromJSON=VP;a$.encode=AC;a$.decode=YC;a$.fromJSON=H2;yZ.encode=DC;yZ.decode=GC;yZ.fromJSON=RP;w8.encode=OC;w8.decode=HC;x$.encode=CC;x$.decode=KC;qW.encode=xP;qW.decode=gP;jC=(()=>{let $=new ArrayBuffer(2);return new DataView($).setInt16(0,256,!0),new Int16Array($)[0]===256})()});class n4{constructor($){this._json=$}get schema(){return this._json.schema}get batches(){return this._json.batches||[]}get dictionaries(){return this._json.dictionaries||[]}}class PW{tee(){return this._getDOMStream().tee()}pipe($,Z){return this._getNodeStream().pipe($,Z)}pipeTo($,Z){return this._getDOMStream().pipeTo($,Z)}pipeThrough($,Z){return this._getDOMStream().pipeThrough($,Z)}_getDOMStream(){return this._DOMStream||(this._DOMStream=this.toDOMStream())}_getNodeStream(){return this._nodeStream||(this._nodeStream=this.toNodeStream())}}var L$,z2;var xY=O(()=>{MQ();NY();L$=Object.freeze({done:!0,value:void 0});z2=class z2 extends PW{constructor(){super();this._values=[],this.resolvers=[],this._closedPromise=new Promise(($)=>this._closedPromiseResolve=$)}get closed(){return this._closedPromise}cancel($){return n(this,void 0,void 0,function*(){yield this.return($)})}write($){if(this._ensureOpen())this.resolvers.length<=0?this._values.push($):this.resolvers.shift().resolve({done:!1,value:$})}abort($){if(this._closedPromiseResolve)this.resolvers.length<=0?this._error={error:$}:this.resolvers.shift().reject({done:!0,value:$})}close(){if(this._closedPromiseResolve){let{resolvers:$}=this;while($.length>0)$.shift().resolve(L$);this._closedPromiseResolve(),this._closedPromiseResolve=void 0}}[Symbol.asyncIterator](){return this}toDOMStream($){return NZ.toDOMStream(this._closedPromiseResolve||this._error?this:this._values,$)}toNodeStream($){return NZ.toNodeStream(this._closedPromiseResolve||this._error?this:this._values,$)}throw($){return n(this,void 0,void 0,function*(){return yield this.abort($),L$})}return($){return n(this,void 0,void 0,function*(){return yield this.close(),L$})}read($){return n(this,void 0,void 0,function*(){return(yield this.next($,"read")).value})}peek($){return n(this,void 0,void 0,function*(){return(yield this.next($,"peek")).value})}next(...$){if(this._values.length>0)return Promise.resolve({done:!1,value:this._values.shift()});else if(this._error)return Promise.reject({done:!0,value:this._error.error});else if(!this._closedPromiseResolve)return Promise.resolve(L$);else return new Promise((Z,Q)=>{this.resolvers.push({resolve:Z,reject:Q})})}_ensureOpen(){if(this._closedPromiseResolve)return!0;throw Error("AsyncQueue is closed")}}});class yP{constructor($){this.source=$}cancel($){this.return($)}peek($){return this.next($,"peek").value}read($){return this.next($,"read").value}next($,Z="read"){return this.source.next({cmd:Z,size:$})}throw($){return Object.create(this.source.throw&&this.source.throw($)||L$)}return($){return Object.create(this.source.return&&this.source.return($)||L$)}}class UJ{constructor($){this.source=$,this._closedPromise=new Promise((Z)=>this._closedPromiseResolve=Z)}cancel($){return n(this,void 0,void 0,function*(){yield this.return($)})}get closed(){return this._closedPromise}read($){return n(this,void 0,void 0,function*(){return(yield this.next($,"read")).value})}peek($){return n(this,void 0,void 0,function*(){return(yield this.next($,"peek")).value})}next($){return n(this,arguments,void 0,function*(Z,Q="read"){return yield this.source.next({cmd:Q,size:Z})})}throw($){return n(this,void 0,void 0,function*(){let Z=this.source.throw&&(yield this.source.throw($))||L$;return this._closedPromiseResolve&&this._closedPromiseResolve(),this._closedPromiseResolve=void 0,Object.create(Z)})}return($){return n(this,void 0,void 0,function*(){let Z=this.source.return&&(yield this.source.return($))||L$;return this._closedPromiseResolve&&this._closedPromiseResolve(),this._closedPromiseResolve=void 0,Object.create(Z)})}}var wX,SX,Z8;var J6=O(()=>{MQ();NY();jX();xY();s$();wX=class wX extends z2{write($){if(($=$0($)).byteLength>0)return super.write($)}toString($=!1){return $?n6(this.toUint8Array(!0)):this.toUint8Array(!1).then(n6)}toUint8Array($=!1){return $?AQ(this._values)[0]:(()=>n(this,void 0,void 0,function*(){var Z,Q,X,J;let W=[],Y=0;try{for(var G=!0,H=F8(this),z;z=yield H.next(),Z=z.done,!Z;G=!0){J=z.value,G=!1;let q=J;W.push(q),Y+=q.byteLength}}catch(q){Q={error:q}}finally{try{if(!G&&!Z&&(X=H.return))yield X.call(H)}finally{if(Q)throw Q.error}}return AQ(W,Y)[0]}))()}};SX=class SX{constructor($){if($)this.source=new yP(NZ.fromIterable($))}[Symbol.iterator](){return this}next($){return this.source.next($)}throw($){return this.source.throw($)}return($){return this.source.return($)}peek($){return this.source.peek($)}read($){return this.source.read($)}};Z8=class Z8{constructor($){if($ instanceof Z8)this.source=$.source;else if($ instanceof wX)this.source=new UJ(NZ.fromAsyncIterable($));else if(Q4($))this.source=new UJ(NZ.fromNodeStream($));else if(PY($))this.source=new UJ(NZ.fromDOMStream($));else if($4($))this.source=new UJ(NZ.fromDOMStream($.body));else if(U8($))this.source=new UJ(NZ.fromIterable($));else if(N8($))this.source=new UJ(NZ.fromAsyncIterable($));else if(fQ($))this.source=new UJ(NZ.fromAsyncIterable($))}[Symbol.asyncIterator](){return this}next($){return this.source.next($)}throw($){return this.source.throw($)}return($){return this.source.return($)}get closed(){return this.source.closed}cancel($){return this.source.cancel($)}peek($){return this.source.peek($)}read($){return this.source.read($)}}});var o4,W6;var q2=O(()=>{MQ();J6();s$();o4=class o4 extends SX{constructor($,Z){super();this.position=0,this.buffer=$0($),this.size=Z===void 0?this.buffer.byteLength:Z}readInt32($){let{buffer:Z,byteOffset:Q}=this.readAt($,4);return new DataView(Z,Q).getInt32(0,!0)}seek($){return this.position=Math.min($,this.size),$n(this,void 0,void 0,function*(){this.size=(yield $.stat()).size,delete this._pending}))()}readInt32($){return n(this,void 0,void 0,function*(){let{buffer:Z,byteOffset:Q}=yield this.readAt($,4);return new DataView(Z,Q).getInt32(0,!0)})}seek($){return n(this,void 0,void 0,function*(){return this._pending&&(yield this._pending),this.position=Math.min($,this.size),$$$,Int64:()=>SZ,Int128:()=>Q8,BaseInt64:()=>s4});function FW($){if($<0)$=4294967295+$+1;return`0x${$.toString(16)}`}class s4{constructor($){this.buffer=$}high(){return this.buffer[1]}low(){return this.buffer[0]}_times($){let Z=new Uint32Array([this.buffer[1]>>>16,this.buffer[1]&65535,this.buffer[0]>>>16,this.buffer[0]&65535]),Q=new Uint32Array([$.buffer[1]>>>16,$.buffer[1]&65535,$.buffer[0]>>>16,$.buffer[0]&65535]),X=Z[3]*Q[3];this.buffer[0]=X&65535;let J=X>>>16;return X=Z[2]*Q[3],J+=X,X=Z[3]*Q[2]>>>0,J+=X,this.buffer[0]+=J<<16,this.buffer[1]=J>>>0>>16,this.buffer[1]+=Z[1]*Q[3]+Z[2]*Q[2]+Z[3]*Q[1],this.buffer[1]+=Z[0]*Q[3]+Z[1]*Q[2]+Z[2]*Q[1]+Z[3]*Q[0]<<16,this}_plus($){let Z=this.buffer[0]+$.buffer[0]>>>0;if(this.buffer[1]+=$.buffer[1],Z>>0)++this.buffer[1];this.buffer[0]=Z}lessThan($){return this.buffer[1]<$.buffer[1]||this.buffer[1]===$.buffer[1]&&this.buffer[0]<$.buffer[0]}equals($){return this.buffer[1]===$.buffer[1]&&this.buffer[0]==$.buffer[0]}greaterThan($){return $.lessThan(this)}hex(){return`${FW(this.buffer[1])} ${FW(this.buffer[0])}`}}class Q8{constructor($){this.buffer=$}high(){return new SZ(new Uint32Array(this.buffer.buffer,this.buffer.byteOffset+8,2))}low(){return new SZ(new Uint32Array(this.buffer.buffer,this.buffer.byteOffset,2))}negate(){if(this.buffer[0]=~this.buffer[0]+1,this.buffer[1]=~this.buffer[1],this.buffer[2]=~this.buffer[2],this.buffer[3]=~this.buffer[3],this.buffer[0]==0)++this.buffer[1];if(this.buffer[1]==0)++this.buffer[2];if(this.buffer[2]==0)++this.buffer[3];return this}times($){let Z=new $$(new Uint32Array([this.buffer[3],0])),Q=new $$(new Uint32Array([this.buffer[2],0])),X=new $$(new Uint32Array([this.buffer[1],0])),J=new $$(new Uint32Array([this.buffer[0],0])),W=new $$(new Uint32Array([$.buffer[3],0])),Y=new $$(new Uint32Array([$.buffer[2],0])),G=new $$(new Uint32Array([$.buffer[1],0])),H=new $$(new Uint32Array([$.buffer[0],0])),z=$$.multiply(J,H);this.buffer[0]=z.low();let q=new $$(new Uint32Array([z.high(),0]));return z=$$.multiply(X,H),q.plus(z),z=$$.multiply(J,G),q.plus(z),this.buffer[1]=q.low(),this.buffer[3]=q.lessThan(z)?1:0,this.buffer[2]=q.high(),new $$(new Uint32Array(this.buffer.buffer,this.buffer.byteOffset+8,2)).plus($$.multiply(Q,H)).plus($$.multiply(X,G)).plus($$.multiply(J,Y)),this.buffer[3]+=$$.multiply(Z,H).plus($$.multiply(Q,G)).plus($$.multiply(X,Y)).plus($$.multiply(J,W)).low(),this}plus($){let Z=new Uint32Array(4);if(Z[3]=this.buffer[3]+$.buffer[3]>>>0,Z[2]=this.buffer[2]+$.buffer[2]>>>0,Z[1]=this.buffer[1]+$.buffer[1]>>>0,Z[0]=this.buffer[0]+$.buffer[0]>>>0,Z[0]>>0)++Z[1];if(Z[1]>>0)++Z[2];if(Z[2]>>0)++Z[3];return this.buffer[3]=Z[3],this.buffer[2]=Z[2],this.buffer[1]=Z[1],this.buffer[0]=Z[0],this}hex(){return`${FW(this.buffer[3])} ${FW(this.buffer[2])} ${FW(this.buffer[1])} ${FW(this.buffer[0])}`}static multiply($,Z){return new Q8(new Uint32Array($.buffer)).times(Z)}static add($,Z){return new Q8(new Uint32Array($.buffer)).plus(Z)}static from($,Z=new Uint32Array(4)){return Q8.fromString(typeof $==="string"?$:$.toString(),Z)}static fromNumber($,Z=new Uint32Array(4)){return Q8.fromString($.toString(),Z)}static fromString($,Z=new Uint32Array(4)){let Q=$.startsWith("-"),X=$.length,J=new Q8(Z);for(let W=Q?1:0;W{P2=[1,10,100,1000,1e4,1e5,1e6,1e7,1e8];$$=class $$ extends s4{times($){return this._times($),this}plus($){return this._plus($),this}static from($,Z=new Uint32Array(2)){return $$.fromString(typeof $==="string"?$:$.toString(),Z)}static fromNumber($,Z=new Uint32Array(2)){return $$.fromString($.toString(),Z)}static fromString($,Z=new Uint32Array(2)){let Q=$.length,X=new $$(Z);for(let J=0;JRC,toIntervalMonthDayNanoInt32Array:()=>M2,toIntervalDayTimeObjects:()=>VC,toIntervalDayTimeInt32Array:()=>U2});function U2($){var Z,Q;let X=$.length,J=new Int32Array(X*2);for(let W=0,Y=0;W>>0,J[Y++]=Number(z>>BigInt(32))>>>0}else Y+=2}return J}function VC($){let Z=$.length,Q=Array(Z/2);for(let X=0,J=0;X>>0);X[W++]={months:$[J],days:$[J+1],nanoseconds:Z?`${Y}`:Y}}return X}function hP($){let Z=new Uint8Array($.length/2);for(let Q=0;Q<$.length;Q+=2)Z[Q>>1]=Number.parseInt($.slice(Q,Q+2),16);return Z}function EC($){return hP($.join(""))}function bP($,Z){let Q=new Uint8Array($.length*16),X=new DataView(Q.buffer);for(let[J,W]of $.entries()){let Y=J*16,G=W.SIZE;if(X.setInt32(Y,G,!0),W.INLINED!==void 0){let H=Z(W.INLINED);for(let z=0;z>1)]=Number.parseInt(H.slice(z,z+2),16);X.setInt32(Y+8,W.BUFFER_INDEX,!0),X.setInt32(Y+12,W.OFFSET,!0)}}return Q}function wC($){return bP($,(Z)=>{let Q=new Uint8Array(Z.length/2);for(let X=0;X>1]=Number.parseInt(Z.slice(X,X+2),16);return Q})}function SC($){return bP($,(Z)=>{return new TextEncoder().encode(Z)})}var vY,A2,D2;var mP=O(()=>{$8();vZ();H$();R8();GW();jX();N2();u$();s$();vY=class vY extends H0{constructor($,Z,Q,X,J=i0.V5,W=[]){super();this.nodesIndex=-1,this.buffersIndex=-1,this.variadicBufferIndex=-1,this.bytes=$,this.nodes=Z,this.buffers=Q,this.dictionaries=X,this.metadataVersion=J,this.variadicBufferCounts=W}visit($){return super.visit($ instanceof a?$.type:$)}visitNull($,{length:Z}=this.nextFieldNode()){return I({type:$,length:Z})}visitBool($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),data:this.readData($)})}visitInt($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),data:this.readData($)})}visitFloat($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),data:this.readData($)})}visitUtf8($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),valueOffsets:this.readOffsets($),data:this.readData($)})}visitLargeUtf8($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),valueOffsets:this.readOffsets($),data:this.readData($)})}visitUtf8View($,{length:Z,nullCount:Q}=this.nextFieldNode()){let X=this.readNullBitmap($,Q),J=this.readData($),W=this.readVariadicBuffers(this.nextVariadicBufferCount());return I({type:$,length:Z,nullCount:Q,nullBitmap:X,["views"]:J,["variadicBuffers"]:W})}visitBinary($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),valueOffsets:this.readOffsets($),data:this.readData($)})}visitLargeBinary($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),valueOffsets:this.readOffsets($),data:this.readData($)})}visitBinaryView($,{length:Z,nullCount:Q}=this.nextFieldNode()){let X=this.readNullBitmap($,Q),J=this.readData($),W=this.readVariadicBuffers(this.nextVariadicBufferCount());return I({type:$,length:Z,nullCount:Q,nullBitmap:X,["views"]:J,["variadicBuffers"]:W})}visitFixedSizeBinary($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),data:this.readData($)})}visitDate($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),data:this.readData($)})}visitTimestamp($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),data:this.readData($)})}visitTime($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),data:this.readData($)})}visitDecimal($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),data:this.readData($)})}visitList($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),valueOffsets:this.readOffsets($),child:this.visit($.children[0])})}visitStruct($,{length:Z,nullCount:Q}=this.nextFieldNode()){return I({type:$,length:Z,nullCount:Q,nullBitmap:this.readNullBitmap($,Q),children:this.visitMany($.children)})}visitUnion($,{length:Z,nullCount:Q}=this.nextFieldNode()){if(this.metadataVersion0&&this.readData($,Q)||new Uint8Array(0)}readOffsets($,Z){return this.readData($,Z)}readTypeIds($,Z){return this.readData($,Z)}readData($,{length:Z,offset:Q}=this.nextBufferRange()){return this.bytes.subarray(Q,Q+Z)}readVariadicBuffers($){return Array.from({length:$},()=>this.readData(null))}nextVariadicBufferCount(){var $;return($=this.variadicBufferCounts[++this.variadicBufferIndex])!==null&&$!==void 0?$:0}readDictionary($){return this.dictionaries.get($.id)}};A2=class A2 extends vY{constructor($,Z,Q,X,J,W=[]){super(new Uint8Array(0),Z,Q,X,J,W);this.sources=$}readNullBitmap($,Z,{offset:Q}=this.nextBufferRange()){return Z<=0?new Uint8Array(0):$6(this.sources[Q])}readOffsets($,{offset:Z}=this.nextBufferRange()){return S0(Uint8Array,S0($.OffsetArrayType,this.sources[Z]))}readTypeIds($,{offset:Z}=this.nextBufferRange()){return S0(Uint8Array,S0($.ArrayType,this.sources[Z]))}readData($,{offset:Z}=this.nextBufferRange()){let{sources:Q}=this;if(k.isTimestamp($))return S0(Uint8Array,SZ.convertArray(Q[Z]));else if((k.isInt($)||k.isTime($))&&$.bitWidth===64||k.isDuration($))return S0(Uint8Array,SZ.convertArray(Q[Z]));else if(k.isDate($)&&$.unit===r0.MILLISECOND)return S0(Uint8Array,SZ.convertArray(Q[Z]));else if(k.isDecimal($))return S0(Uint8Array,Q8.convertArray(Q[Z]));else if(k.isBinary($)||k.isLargeBinary($)||k.isFixedSizeBinary($))return EC(Q[Z]);else if(k.isBinaryView($))return wC(Q[Z]);else if(k.isUtf8View($))return SC(Q[Z]);else if(k.isBool($))return $6(Q[Z]);else if(k.isUtf8($)||k.isLargeUtf8($))return LQ(Q[Z].join(""));else if(k.isInterval($))switch($.unit){case s0.DAY_TIME:return U2(Q[Z]);case s0.MONTH_DAY_NANO:return M2(Q[Z]);default:break}return S0(Uint8Array,S0($.ArrayType,Q[Z].map((X)=>+X)))}readVariadicBuffers($){let Z=[];for(let Q=0;Q<$;Q++){let{offset:X}=this.nextBufferRange(),J=this.sources[X];Z.push(hP(J))}return Z}};D2=class D2 extends vY{constructor($,Z,Q,X,J,W=[]){super(new Uint8Array(0),Z,Q,X,J,W);this.bodyChunks=$}readData($,Z=this.nextBufferRange()){return this.bodyChunks[this.buffersIndex]}}});var yY;var O2=O(()=>{$X();R$();s$();yY=class yY extends SQ{constructor($){super($);this._values=new wQ(Uint8Array)}get byteLength(){let $=this._pendingLength+this.length*4;return this._offsets&&($+=this._offsets.byteLength),this._values&&($+=this._values.byteLength),this._nulls&&($+=this._nulls.byteLength),$}setValue($,Z){return super.setValue($,$0(Z))}_flushPending($,Z){let Q=this._offsets,X=this._values.reserve(Z).buffer,J=0;for(let[W,Y]of $)if(Y===void 0)Q.set(W,0);else{let G=Y.length;X.set(Y,J),Q.set(W,G),J+=G}}}});var hY;var C2=O(()=>{s$();$X();R$();hY=class hY extends SQ{constructor($){super($);this._values=new wQ(Uint8Array)}get byteLength(){let $=this._pendingLength+this.length*4;return this._offsets&&($+=this._offsets.byteLength),this._values&&($+=this._values.byteLength),this._nulls&&($+=this._nulls.byteLength),$}setValue($,Z){return super.setValue($,$0(Z))}_flushPending($,Z){let Q=this._offsets,X=this._values.reserve(Z).buffer,J=0;for(let[W,Y]of $)if(Y===void 0)Q.set(W,BigInt(0));else{let G=Y.length;X.set(Y,J),Q.set(W,BigInt(G)),J+=G}}}});var j2;var fP=O(()=>{$X();R$();j2=class j2 extends M${constructor($){super($);this._values=new TY}setValue($,Z){this._values.set($,+Z)}}});var UW,i4,r4;var uP=O(()=>{R$();EQ();UW=class UW extends r${};UW.prototype._setValue=iK;i4=class i4 extends UW{};i4.prototype._setValue=q4;r4=class r4 extends UW{};r4.prototype._setValue=P4});var a4;var pP=O(()=>{R$();EQ();a4=class a4 extends r${};a4.prototype._setValue=tK});var V2;var cP=O(()=>{H$();R$();t4();V2=class V2 extends M${constructor({type:$,nullValues:Z,dictionaryHashFunction:Q}){super({type:new YZ($.dictionary,$.indices,$.id,$.isOrdered)});if(this._nulls=null,this._dictionaryOffset=0,this._keysToIndices=Object.create(null),this.indices=BX({type:this.type.indices,nullValues:Z}),this.dictionary=BX({type:this.type.dictionary,nullValues:null}),typeof Q==="function")this.valueToKey=Q}get values(){return this.indices.values}get nullCount(){return this.indices.nullCount}get nullBitmap(){return this.indices.nullBitmap}get byteLength(){return this.indices.byteLength+this.dictionary.byteLength}get reservedLength(){return this.indices.reservedLength+this.dictionary.reservedLength}get reservedByteLength(){return this.indices.reservedByteLength+this.dictionary.reservedByteLength}isValid($){return this.indices.isValid($)}setValid($,Z){let Q=this.indices;return Z=Q.setValid($,Z),this.length=Q.length,Z}setValue($,Z){let Q=this._keysToIndices,X=this.valueToKey(Z),J=Q[X];if(J===void 0)Q[X]=J=this._dictionaryOffset+this.dictionary.append(Z).length-1;return this.indices.setValue($,J)}flush(){let $=this.type,Z=this._dictionary,Q=this.dictionary.toVector(),X=this.indices.flush().clone($);return X.dictionary=Z?Z.concat(Q):Q,this.finished||(this._dictionaryOffset+=Q.length),this._dictionary=X.dictionary,this.clear(),X}finish(){return this.indices.finish(),this.dictionary.finish(),this._dictionaryOffset=0,this._keysToIndices=Object.create(null),super.finish()}clear(){return this.indices.clear(),this.dictionary.clear(),super.clear()}valueToKey($){return typeof $==="string"?$:`${$}`}}});var e4;var lP=O(()=>{R$();EQ();e4=class e4 extends r${};e4.prototype._setValue=sK});var R2;var dP=O(()=>{vZ();R$();H$();R2=class R2 extends M${setValue($,Z){let[Q]=this.children,X=$*this.stride;for(let J=-1,W=this.stride;++J0)throw Error("FixedSizeListBuilder can only have one child.");let Q=this.children.push($);return this.type=new jQ(this.type.listSize,new a(Z,$.type,!0)),Q}}});var MW,E2,w2,S2;var nP=O(()=>{SY();R$();MW=class MW extends r${setValue($,Z){this._values.set($,Z)}};E2=class E2 extends MW{setValue($,Z){super.setValue($,wY(Z))}};w2=class w2 extends MW{};S2=class S2 extends MW{}});var Y6,$G,ZG,QG;var oP=O(()=>{R$();EQ();Y6=class Y6 extends r${};Y6.prototype._setValue=eK;$G=class $G extends Y6{};$G.prototype._setValue=C4;ZG=class ZG extends Y6{};ZG.prototype._setValue=j4;QG=class QG extends Y6{};QG.prototype._setValue=V4});var MJ,XG,JG,WG,YG;var sP=O(()=>{R$();EQ();MJ=class MJ extends r${};MJ.prototype._setValue=$2;XG=class XG extends MJ{};XG.prototype._setValue=R4;JG=class JG extends MJ{};JG.prototype._setValue=E4;WG=class WG extends MJ{};WG.prototype._setValue=w4;YG=class YG extends MJ{};YG.prototype._setValue=S4});var S8,B2,k2,I2,_2,T2,g2,x2,v2;var iP=O(()=>{R$();S8=class S8 extends r${setValue($,Z){this._values.set($,Z)}};B2=class B2 extends S8{};k2=class k2 extends S8{};I2=class I2 extends S8{};_2=class _2 extends S8{};T2=class T2 extends S8{};g2=class g2 extends S8{};x2=class x2 extends S8{};v2=class v2 extends S8{}});var y2;var rP=O(()=>{vZ();H$();$X();R$();y2=class y2 extends SQ{constructor($){super($);this._offsets=new gY($.type)}addChild($,Z="0"){if(this.numChildren>0)throw Error("ListBuilder can only have one child.");return this.children[this.numChildren]=$,this.type=new LZ(new a(Z,$.type,!0)),this.numChildren-1}_flushPending($){let Z=this._offsets,[Q]=this.children;for(let[X,J]of $)if(typeof J>"u")Z.set(X,0);else{let W=J,Y=W.length,G=Z.set(X,Y).buffer[X];for(let H=-1;++H{vZ();H$();R$();h2=class h2 extends SQ{set($,Z){return super.set($,Z)}setValue($,Z){let Q=Z instanceof Map?Z:new Map(Object.entries(Z)),X=this._pending||(this._pending=new Map),J=X.get($);J&&(this._pendingLength-=J.size),this._pendingLength+=Q.size,X.set($,Q)}addChild($,Z=`${this.numChildren}`){if(this.numChildren>0)throw Error("ListBuilder can only have one child.");return this.children[this.numChildren]=$,this.type=new eZ(new a(Z,$.type,!0),this.type.keysSorted),this.numChildren-1}_flushPending($){let Z=this._offsets,[Q]=this.children;for(let[X,J]of $)if(J===void 0)Z.set(X,0);else{let{[X]:W,[X+1]:Y}=Z.set(X,J.size).buffer;for(let G of J.entries())if(Q.set(W,G),++W>=Y)break}}}});var b2;var tP=O(()=>{R$();b2=class b2 extends M${setValue($,Z){}setValid($,Z){return this.length=Math.max($+1,this.length),Z}}});var m2;var eP=O(()=>{vZ();R$();H$();m2=class m2 extends M${setValue($,Z){let{children:Q,type:X}=this;switch(Array.isArray(Z)||Z.constructor){case!0:return X.children.forEach((J,W)=>Q[W].set($,Z[W]));case Map:return X.children.forEach((J,W)=>Q[W].set($,Z.get(J.name)));default:return X.children.forEach((J,W)=>Q[W].set($,Z[J.name]))}}setValid($,Z){if(!super.setValid($,Z))this.children.forEach((Q)=>Q.setValid($,Z));return Z}addChild($,Z=`${this.numChildren}`){let Q=this.children.push($);return this.type=new j0([...this.type.children,new a(Z,$.type,!0)]),Q}}});var LJ,GG,KG,HG,zG;var $F=O(()=>{R$();EQ();LJ=class LJ extends r${};LJ.prototype._setValue=rK;GG=class GG extends LJ{};GG.prototype._setValue=F4;KG=class KG extends LJ{};KG.prototype._setValue=N4;HG=class HG extends LJ{};HG.prototype._setValue=U4;zG=class zG extends LJ{};zG.prototype._setValue=M4});var AJ,qG,PG,FG,NG;var ZF=O(()=>{R$();EQ();AJ=class AJ extends r${};AJ.prototype._setValue=aK;qG=class qG extends AJ{};qG.prototype._setValue=L4;PG=class PG extends AJ{};PG.prototype._setValue=A4;FG=class FG extends AJ{};FG.prototype._setValue=D4;NG=class NG extends AJ{};NG.prototype._setValue=O4});var bY,f2,u2;var QF=O(()=>{vZ();$X();R$();H$();bY=class bY extends M${constructor($){super($);if(this._typeIds=new FJ(Int8Array,0,1),typeof $.valueToChildTypeId==="function")this._valueToChildTypeId=$.valueToChildTypeId}get typeIdToChildIndex(){return this.type.typeIdToChildIndex}append($,Z){return this.set(this.length,$,Z)}set($,Z,Q){if(Q===void 0)Q=this._valueToChildTypeId(this,Z,$);return this.setValue($,Z,Q),this}setValue($,Z,Q){this._typeIds.set($,Q);let X=this.type.typeIdToChildIndex[Q],J=this.children[X];J===null||J===void 0||J.set($,Z),this.length=Math.max($+1,this.length)}addChild($,Z=`${this.children.length}`){let Q=this.children.push($),{type:{children:X,mode:J,typeIds:W}}=this,Y=[...X,new a(Z,$.type)];return this.type=new CQ(J,[...W,Q],Y),Q}_valueToChildTypeId($,Z,Q){throw Error("Cannot map UnionBuilder value to child typeId. Pass the `childTypeId` as the second argument to unionBuilder.append(), or supply a `valueToChildTypeId` function as part of the UnionBuilder constructor options.")}};f2=class f2 extends bY{};u2=class u2 extends bY{constructor($){super($);this._offsets=new FJ(Int32Array)}setValue($,Z,Q){let X=this._typeIds.set($,Q).buffer[$],J=this.getChildAt(this.type.typeIdToChildIndex[X]),W=this._offsets.set($,J.length).buffer[$];J===null||J===void 0||J.set(W,Z),this.length=Math.max($+1,this.length)}}});var UG;var XF=O(()=>{jX();O2();$X();R$();UG=class UG extends SQ{constructor($){super($);this._values=new wQ(Uint8Array)}get byteLength(){let $=this._pendingLength+this.length*4;return this._offsets&&($+=this._offsets.byteLength),this._values&&($+=this._values.byteLength),this._nulls&&($+=this._nulls.byteLength),$}setValue($,Z){return super.setValue($,LQ(Z))}_flushPending($,Z){}};UG.prototype._flushPending=yY.prototype._flushPending});var MG;var JF=O(()=>{jX();$X();R$();C2();MG=class MG extends SQ{constructor($){super($);this._values=new wQ(Uint8Array)}get byteLength(){let $=this._pendingLength+this.length*4;return this._offsets&&($+=this._offsets.byteLength),this._values&&($+=this._values.byteLength),this._nulls&&($+=this._nulls.byteLength),$}setValue($,Z){return super.setValue($,LQ(Z))}_flushPending($,Z){}};MG.prototype._flushPending=hY.prototype._flushPending});var mY;var p2=O(()=>{H$();R$();$X();s$();$8();mY=class mY extends M${constructor($){super($);this._variadicBuffers=[],this._currentBuffer=null,this._currentBufferIndex=0,this._currentBufferOffset=0,this._bufferSize=33554432,this._views=new wQ(Uint8Array)}get byteLength(){let $=0;this._views&&($+=this._views.byteLength),this._nulls&&($+=this._nulls.byteLength);for(let Z of this._variadicBuffers)$+=Z.byteLength;return this._currentBuffer&&($+=this._currentBuffer.byteLength),$}setValue($,Z){return this.writeBinaryValue($,this.encodeValue(Z))}writeBinaryValue($,Z){let Q=Z.length,X=($+1)*R0.ELEMENT_WIDTH,J=this._views.length;if(X>J)this._views.reserve(X-J);let W=this._views.buffer,Y=$*R0.ELEMENT_WIDTH,G=new DataView(W.buffer,W.byteOffset+Y,R0.ELEMENT_WIDTH);if(G.setInt32(R0.LENGTH_OFFSET,Q,!0),Q<=R0.INLINE_CAPACITY){W.set(Z,Y+R0.INLINE_OFFSET);for(let H=Q;Hthis._bufferSize){if(this._currentBuffer)this._variadicBuffers.push(this._currentBuffer.buffer.slice(0,this._currentBufferOffset));this._currentBuffer=new wQ(Uint8Array),this._currentBufferIndex=this._variadicBuffers.length,this._currentBufferOffset=0}this._currentBuffer.reserve(Q).buffer.set(Z,this._currentBufferOffset),G.setInt32(R0.BUFFER_INDEX_OFFSET,this._currentBufferIndex,!0),G.setInt32(R0.BUFFER_OFFSET_OFFSET,this._currentBufferOffset,!0),this._currentBufferOffset+=Q}return this}encodeValue($){return $0($)}setValid($,Z){let Q=($+1)*R0.ELEMENT_WIDTH,X=this._views.length;if(Q>X)this._views.reserve(Q-X);let J=super.setValid($,Z);if(!J){let W=this._views.buffer,Y=$*R0.ELEMENT_WIDTH;for(let G=0;G0)this._variadicBuffers.push(this._currentBuffer.buffer.slice(0,this._currentBufferOffset)),this._currentBuffer=null,this._currentBufferOffset=0;let W=X.flush(Z*R0.ELEMENT_WIDTH),Y=Q>0?J.flush(Z):void 0,G=this._variadicBuffers.slice();return this._variadicBuffers=[],this._currentBufferIndex=0,this.clear(),I({type:$,length:Z,nullCount:Q,nullBitmap:Y,["views"]:W,["variadicBuffers"]:G})}finish(){return this.finished=!0,this}}});var c2;var WF=O(()=>{p2();jX();c2=class c2 extends mY{constructor($){super($)}setValue($,Z){return this.writeBinaryValue($,LQ(Z))}}});var YF,GF;var KF=O(()=>{R8();O2();C2();fP();uP();pP();cP();lP();dP();nP();oP();sP();iP();rP();aP();tP();eP();$F();ZF();QF();XF();JF();p2();WF();YF=class YF extends H0{visitNull(){return b2}visitBool(){return j2}visitInt(){return S8}visitInt8(){return B2}visitInt16(){return k2}visitInt32(){return I2}visitInt64(){return _2}visitUint8(){return T2}visitUint16(){return g2}visitUint32(){return x2}visitUint64(){return v2}visitFloat(){return MW}visitFloat16(){return E2}visitFloat32(){return w2}visitFloat64(){return S2}visitUtf8(){return UG}visitLargeUtf8(){return MG}visitBinary(){return yY}visitLargeBinary(){return hY}visitFixedSizeBinary(){return e4}visitDate(){return UW}visitDateDay(){return i4}visitDateMillisecond(){return r4}visitTimestamp(){return LJ}visitTimestampSecond(){return GG}visitTimestampMillisecond(){return KG}visitTimestampMicrosecond(){return HG}visitTimestampNanosecond(){return zG}visitTime(){return AJ}visitTimeSecond(){return qG}visitTimeMillisecond(){return PG}visitTimeMicrosecond(){return FG}visitTimeNanosecond(){return NG}visitDecimal(){return a4}visitList(){return y2}visitStruct(){return m2}visitUnion(){return bY}visitDenseUnion(){return u2}visitSparseUnion(){return f2}visitDictionary(){return V2}visitInterval(){return Y6}visitIntervalDayTime(){return $G}visitIntervalYearMonth(){return ZG}visitIntervalMonthDayNano(){return QG}visitDuration(){return MJ}visitDurationSecond(){return XG}visitDurationMillisecond(){return JG}visitDurationMicrosecond(){return WG}visitDurationNanosecond(){return YG}visitFixedSizeList(){return R2}visitMap(){return h2}visitBinaryView(){return mY}visitUtf8View(){return c2}};GF=new YF});function hZ($,Z){return Z instanceof $.constructor}function kX($,Z){return $===Z||hZ($,Z)}function IX($,Z){return $===Z||hZ($,Z)&&$.bitWidth===Z.bitWidth&&$.isSigned===Z.isSigned}function LG($,Z){return $===Z||hZ($,Z)&&$.precision===Z.precision}function BC($,Z){return $===Z||hZ($,Z)&&$.byteWidth===Z.byteWidth}function l2($,Z){return $===Z||hZ($,Z)&&$.unit===Z.unit}function fY($,Z){return $===Z||hZ($,Z)&&$.unit===Z.unit&&$.timezone===Z.timezone}function uY($,Z){return $===Z||hZ($,Z)&&$.unit===Z.unit&&$.bitWidth===Z.bitWidth}function kC($,Z){return $===Z||hZ($,Z)&&$.children.length===Z.children.length&&ZX.compareManyFields($.children,Z.children)}function IC($,Z){return $===Z||hZ($,Z)&&$.children.length===Z.children.length&&ZX.compareManyFields($.children,Z.children)}function d2($,Z){return $===Z||hZ($,Z)&&$.mode===Z.mode&&$.typeIds.every((Q,X)=>Q===Z.typeIds[X])&&ZX.compareManyFields($.children,Z.children)}function _C($,Z){return $===Z||hZ($,Z)&&$.id===Z.id&&$.isOrdered===Z.isOrdered&&ZX.visit($.indices,Z.indices)&&ZX.visit($.dictionary,Z.dictionary)}function AG($,Z){return $===Z||hZ($,Z)&&$.unit===Z.unit}function pY($,Z){return $===Z||hZ($,Z)&&$.unit===Z.unit}function TC($,Z){return $===Z||hZ($,Z)&&$.listSize===Z.listSize&&$.children.length===Z.children.length&&ZX.compareManyFields($.children,Z.children)}function gC($,Z){return $===Z||hZ($,Z)&&$.keysSorted===Z.keysSorted&&$.children.length===Z.children.length&&ZX.compareManyFields($.children,Z.children)}function G6($,Z){return ZX.compareSchemas($,Z)}function HF($,Z){return ZX.compareFields($,Z)}function DG($,Z){return ZX.visit($,Z)}var G0,ZX;var cY=O(()=>{R8();G0=class G0 extends H0{compareSchemas($,Z){return $===Z||Z instanceof $.constructor&&this.compareManyFields($.fields,Z.fields)}compareManyFields($,Z){return $===Z||Array.isArray($)&&Array.isArray(Z)&&$.length===Z.length&&$.every((Q,X)=>this.compareFields(Q,Z[X]))}compareFields($,Z){return $===Z||Z instanceof $.constructor&&$.name===Z.name&&$.nullable===Z.nullable&&this.visit($.type,Z.type)}};G0.prototype.visitNull=kX;G0.prototype.visitBool=kX;G0.prototype.visitInt=IX;G0.prototype.visitInt8=IX;G0.prototype.visitInt16=IX;G0.prototype.visitInt32=IX;G0.prototype.visitInt64=IX;G0.prototype.visitUint8=IX;G0.prototype.visitUint16=IX;G0.prototype.visitUint32=IX;G0.prototype.visitUint64=IX;G0.prototype.visitFloat=LG;G0.prototype.visitFloat16=LG;G0.prototype.visitFloat32=LG;G0.prototype.visitFloat64=LG;G0.prototype.visitUtf8=kX;G0.prototype.visitLargeUtf8=kX;G0.prototype.visitUtf8View=kX;G0.prototype.visitBinary=kX;G0.prototype.visitLargeBinary=kX;G0.prototype.visitBinaryView=kX;G0.prototype.visitFixedSizeBinary=BC;G0.prototype.visitDate=l2;G0.prototype.visitDateDay=l2;G0.prototype.visitDateMillisecond=l2;G0.prototype.visitTimestamp=fY;G0.prototype.visitTimestampSecond=fY;G0.prototype.visitTimestampMillisecond=fY;G0.prototype.visitTimestampMicrosecond=fY;G0.prototype.visitTimestampNanosecond=fY;G0.prototype.visitTime=uY;G0.prototype.visitTimeSecond=uY;G0.prototype.visitTimeMillisecond=uY;G0.prototype.visitTimeMicrosecond=uY;G0.prototype.visitTimeNanosecond=uY;G0.prototype.visitDecimal=kX;G0.prototype.visitList=kC;G0.prototype.visitStruct=IC;G0.prototype.visitUnion=d2;G0.prototype.visitDenseUnion=d2;G0.prototype.visitSparseUnion=d2;G0.prototype.visitDictionary=_C;G0.prototype.visitInterval=AG;G0.prototype.visitIntervalDayTime=AG;G0.prototype.visitIntervalYearMonth=AG;G0.prototype.visitIntervalMonthDayNano=AG;G0.prototype.visitDuration=pY;G0.prototype.visitDurationSecond=pY;G0.prototype.visitDurationMillisecond=pY;G0.prototype.visitDurationMicrosecond=pY;G0.prototype.visitDurationNanosecond=pY;G0.prototype.visitFixedSizeList=TC;G0.prototype.visitMap=gC;ZX=new G0});function BX($){let Z=$.type,Q=new(GF.getVisitFn(Z)())($);if(Z.children&&Z.children.length>0){let X=$.children||[],J={nullValues:$.nullValues},W=Array.isArray(X)?(Y,G)=>X[G]||J:({name:Y})=>X[Y]||J;for(let[Y,G]of Z.children.entries()){let{type:H}=G,z=W(G,Y);Q.children.push(BX(Object.assign(Object.assign({},z),{type:H})))}}return Q}function QX($,Z){if($ instanceof A0||$ instanceof r||$.type instanceof k)return Q6($);if(ArrayBuffer.isView($)&&!Z)return Q6($);if(ArrayBuffer.isView($)&&Z){let W=$ instanceof BigInt64Array||$ instanceof BigUint64Array,Y=Z.ArrayType===BigInt64Array||Z.ArrayType===BigUint64Array;if(W&&!Y)throw TypeError(`Cannot convert BigInt input to ${Z}. BigInt arrays can only target BigInt-based types (e.g. Int64, Uint64).`);if(!W&&Y)throw TypeError(`Cannot convert non-BigInt input to ${Z}. ${Z} requires BigInt values.`);if(k.isInt(Z)||k.isFloat(Z)){let G=$.constructor===Z.ArrayType?$:new Z.ArrayType($);return Q6({type:Z,data:G,offset:0,length:G.length,nullCount:0})}}let Q={type:Z!==null&&Z!==void 0?Z:OG($),nullValues:[null]},X=[...n2(Q)($)],J=X.length===1?X[0]:X.reduce((W,Y)=>W.concat(Y));if(k.isDictionary(J.type))return J.memoize();return J}function OG($){if($.length===0)return new j$;let Z=0,Q=0,X=0,J=0,W=0,Y=0,G=0,H=0;for(let z of $){if(z==null){++Z;continue}switch(typeof z){case"bigint":++Y;continue;case"boolean":++G;continue;case"number":++J;continue;case"string":++W;continue;case"object":if(Array.isArray(z))++Q;else if(Object.prototype.toString.call(z)==="[object Date]")++H;else++X;continue}throw TypeError("Unable to infer Vector type from input values, explicit type declaration expected.")}if(J+Z===$.length)return new $Q;else if(W+Z===$.length)return new YZ(new T$,new GZ);else if(Y+Z===$.length)return new AZ;else if(G+Z===$.length)return new V$;else if(H+Z===$.length)return new RY;else if(Q+Z===$.length){let z=$,q=OG(z[z.findIndex((P)=>P!=null)]);if(z.every((P)=>P==null||DG(q,OG(P))))return new LZ(new a("",q,!0))}else if(X+Z===$.length){let z=new Map;for(let q of $)for(let P of Object.keys(q))if(!z.has(P)&&q[P]!=null)z.set(P,new a(P,OG([q[P]]),!0));return new j0([...z.values()])}throw TypeError("Unable to infer Vector type from input values, explicit type declaration expected.")}function n2($){let{["queueingStrategy"]:Z="count"}=$,{["highWaterMark"]:Q=Z!=="bytes"?Number.POSITIVE_INFINITY:Math.pow(2,14)}=$,X=Z!=="bytes"?"length":"byteLength";return function*(J){let W=0,Y=BX($);for(let G of J)if(Y.append(G)[X]>=Q)++W&&(yield Y.toVector());if(Y.finish().length>0||W===0)yield Y.toVector()}}var t4=O(()=>{vZ();H$();$8();ZQ();KF();cY()});function CG($,Z){return xC($,Z.map((Q)=>Q.data.concat()))}function xC($,Z){let Q=[...$.fields],X=[],J={numBatches:Z.reduce((P,U)=>Math.max(P,U.length),0)},W=0,Y=0,G=-1,H=Z.length,z,q=[];while(J.numBatches-- >0){for(Y=Number.POSITIVE_INFINITY,G=-1;++G0)X[W++]=I({type:new j0(Q),length:Y,nullCount:0,children:q.slice()})}}return[$=$.assign(Q),X.map((P)=>new D0($,P))]}function vC($,Z,Q,X,J){var W;let Y=(Z+63&-64)>>3;for(let G=-1,H=X.length;++G=Z)if(q===Z)Q[G]=z;else Q[G]=z.slice(0,Z),J.numBatches=Math.max(J.numBatches,X[G].unshift(z.slice(Z,q-Z)));else{let P=$[G];$[G]=P.clone({nullable:!0}),Q[G]=(W=z===null||z===void 0?void 0:z._changeLengthAndBackfillNullBitmap(Z))!==null&&W!==void 0?W:I({type:P.type,length:Z,nullCount:Z,nullBitmap:new Uint8Array(Y)})}}return Q}var zF=O(()=>{$8();H$();DJ()});var qF,PF,v$;var jG=O(()=>{u$();$8();ZQ();vZ();H$();cY();zF();b4();aJ();EQ();m4();f4();WW();DJ();PF=Symbol.for("apache-arrow/Table");v$=class v${static isTable($){return($===null||$===void 0?void 0:$[PF])===!0}constructor(...$){var Z,Q;if($.length===0)return this.batches=[],this.schema=new Q0([]),this._offsets=[0],this;let X,J;if($[0]instanceof Q0)X=$.shift();if($.at(-1)instanceof Uint32Array)J=$.pop();let W=(G)=>{if(G){if(G instanceof D0)return[G];else if(G instanceof v$)return G.batches;else if(G instanceof A0){if(G.type instanceof j0)return[new D0(new Q0(G.type.children),G)]}else if(Array.isArray(G))return G.flatMap((H)=>W(H));else if(typeof G[Symbol.iterator]==="function")return[...G].flatMap((H)=>W(H));else if(typeof G==="object"){let H=Object.keys(G),z=H.map((U)=>new r([G[U]])),q=X!==null&&X!==void 0?X:new Q0(H.map((U,M)=>new a(String(U),z[M].type,z[M].nullable))),[,P]=CG(q,z);return P.length===0?[new D0(G)]:P}}return[]},Y=$.flatMap((G)=>W(G));if(X=(Q=X!==null&&X!==void 0?X:(Z=Y[0])===null||Z===void 0?void 0:Z.schema)!==null&&Q!==void 0?Q:new Q0([]),!(X instanceof Q0))throw TypeError("Table constructor expects a [Schema, RecordBatch[]] pair.");for(let G of Y){if(!(G instanceof D0))throw TypeError("Table constructor expects a [Schema, RecordBatch[]] pair.");if(!G6(X,G.schema))throw TypeError("Table and inner RecordBatch schemas must be equivalent.")}this.schema=X,this.batches=Y,this._offsets=J!==null&&J!==void 0?J:x4(this.data)}get data(){return this.batches.map(({data:$})=>$)}get numCols(){return this.schema.fields.length}get numRows(){return this.data.reduce(($,Z)=>$+Z.length,0)}get nullCount(){if(this._nullCount===-1)this._nullCount=g4(this.data);return this._nullCount}isValid($){return!1}get($){return null}at($){return this.get(tJ($,this.numRows))}set($,Z){return}indexOf($,Z){return-1}[Symbol.iterator](){if(this.batches.length>0)return HW.visit(new r(this.data));return[][Symbol.iterator]()}toArray(){return[...this]}toString(){return`[ + }`;if(t.length!==n.length)e=`if (x !== x) return false; +${e}`;return Function("x",`${e} +return true;`)}function Ex(t){if(typeof t!=="bigint")return Jn(t);return`${Jn(t)}n`}var jm=()=>{};function Uf(t,e){let n=Math.ceil(t)*e-1;return(n-n%64+64||64)/e}function $m(t,e=0){return t.length>=e?t.subarray(0,e):bo(new t.constructor(e),t,0)}class wn{constructor(t,e=0,n=1){this.length=Math.ceil(e/n),this.buffer=new t(this.length),this.stride=n,this.BYTES_PER_ELEMENT=t.BYTES_PER_ELEMENT,this.ArrayType=t}get byteLength(){return Math.ceil(this.length*this.stride)*this.BYTES_PER_ELEMENT}get reservedLength(){return this.buffer.length/this.stride}get reservedByteLength(){return this.buffer.byteLength}set(t,e){return this}append(t){return this.set(this.length,t)}reserve(t){if(t>0){this.length+=t;let e=this.stride,n=this.length*e,r=this.buffer.length;if(n>=r)this._resize(r===0?Uf(n*1,this.BYTES_PER_ELEMENT):Uf(n*2,this.BYTES_PER_ELEMENT))}return this}flush(t=this.length){t=Uf(t*this.stride,this.BYTES_PER_ELEMENT);let e=$m(this.buffer,t);return this.clear(),e}clear(){return this.length=0,this.buffer=new this.ArrayType,this}_resize(t){return this.buffer=$m(this.buffer,t)}}var Ai,Po,ko;var Sr=b(()=>{oe();Ai=class Ai extends wn{last(){return this.get(this.length-1)}get(t){return this.buffer[t]}set(t,e){return this.reserve(t-this.length+1),this.buffer[t*this.stride]=e,this}};Po=class Po extends Ai{constructor(){super(Uint8Array,0,0.125);this.numValid=0}get numInvalid(){return this.length-this.numValid}get(t){return this.buffer[t>>3]>>t%8&1}set(t,e){let{buffer:n}=this.reserve(t-this.length+1),r=t>>3,a=t%8,o=n[r]>>a&1;return e?o===0&&(n[r]|=1<=0)r.fill(r[n],n,t);return r[t]=r[t-1]+e,this}flush(t=this.length-1){if(t>this.length)this.set(t-1,this.BYTES_PER_ELEMENT>4?BigInt(0):0);return super.flush(t+1)}}});class Lt{static throughNode(t){throw Error('"throughNode" not available in this environment')}static throughDOM(t){throw Error('"throughDOM" not available in this environment')}constructor({type:t,nullValues:e}){if(this.length=0,this.finished=!1,this.type=t,this.children=[],this.nullValues=e,this.stride=gn(t),this._nulls=new Po,e&&e.length>0)this._isValid=zm(e)}toVector(){return new O([this.flush()])}get ArrayType(){return this.type.ArrayType}get nullCount(){return this._nulls.numInvalid}get numChildren(){return this.children.length}get byteLength(){let t=0,{_offsets:e,_values:n,_nulls:r,_typeIds:a,children:o}=this;return e&&(t+=e.byteLength),n&&(t+=n.byteLength),r&&(t+=r.byteLength),a&&(t+=a.byteLength),o.reduce((s,c)=>s+c.byteLength,t)}get reservedLength(){return this._nulls.reservedLength}get reservedByteLength(){let t=0;return this._offsets&&(t+=this._offsets.reservedByteLength),this._values&&(t+=this._values.reservedByteLength),this._nulls&&(t+=this._nulls.reservedByteLength),this._typeIds&&(t+=this._typeIds.reservedByteLength),this.children.reduce((e,n)=>e+n.reservedByteLength,t)}get valueOffsets(){return this._offsets?this._offsets.buffer:null}get values(){return this._values?this._values.buffer:null}get nullBitmap(){return this._nulls?this._nulls.buffer:null}get typeIds(){return this._typeIds?this._typeIds.buffer:null}append(t){return this.set(this.length,t)}isValid(t){return this._isValid(t)}set(t,e){if(this.setValid(t,this.isValid(e)))this.setValue(t,e);return this}setValue(t,e){this._setValue(this,t,e)}setValid(t,e){return this.length=this._nulls.set(t,+e).length,e}addChild(t,e=`${this.numChildren}`){throw Error(`Cannot append children to non-nested type "${this.type}"`)}getChildAt(t){return this.children[t]||null}flush(){let t,e,n,r,{type:a,length:o,nullCount:s,_typeIds:c,_offsets:l,_values:f,_nulls:p}=this;if(e=c===null||c===void 0?void 0:c.flush(o))r=l===null||l===void 0?void 0:l.flush(o);else if(r=l===null||l===void 0?void 0:l.flush(o))t=f===null||f===void 0?void 0:f.flush(l.last());else t=f===null||f===void 0?void 0:f.flush(o);if(s>0)n=p===null||p===void 0?void 0:p.flush(o);let _=this.children.map((g)=>g.flush());return this.clear(),A({type:a,length:o,nullCount:s,children:_,child:_[0],data:t,typeIds:e,nullBitmap:n,valueOffsets:r})}finish(){this.finished=!0;for(let t of this.children)t.finish();return this}clear(){var t,e,n,r;this.length=0,(t=this._nulls)===null||t===void 0||t.clear(),(e=this._values)===null||e===void 0||e.clear(),(n=this._offsets)===null||n===void 0||n.clear(),(r=this._typeIds)===null||r===void 0||r.clear();for(let a of this.children)a.clear();return this}}var ce,xn;var $t=b(()=>{je();zn();wc();Ft();jm();Sr();Lt.prototype.length=1;Lt.prototype.stride=1;Lt.prototype.children=null;Lt.prototype.finished=!1;Lt.prototype.nullValues=null;Lt.prototype._isValid=()=>!0;ce=class ce extends Lt{constructor(t){super(t);this._values=new Ai(this.ArrayType,0,this.stride)}setValue(t,e){let n=this._values;return n.reserve(t-n.length+1),super.setValue(t,e)}};xn=class xn extends Lt{constructor(t){super(t);this._pendingLength=0,this._offsets=new ko(t.type)}setValue(t,e){let n=this._pending||(this._pending=new Map),r=n.get(t);r&&(this._pendingLength-=r.length),this._pendingLength+=e instanceof Yr?e[xi].length:e.length,n.set(t,e)}setValid(t,e){if(!super.setValid(t,e))return(this._pending||(this._pending=new Map)).set(t,void 0),!1;return!0}clear(){return this._pendingLength=0,this._pending=void 0,super.clear()}flush(){return this._flush(),super.flush()}finish(){return this._flush(),super.finish()}_flush(){let t=this._pending,e=this._pendingLength;if(this._pendingLength=0,this._pending=void 0,t&&t.size>0)this._flushPending(t,e);return this}}});class Ma{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}offset(){return this.bb.readInt64(this.bb_pos)}metaDataLength(){return this.bb.readInt32(this.bb_pos+8)}bodyLength(){return this.bb.readInt64(this.bb_pos+16)}static sizeOf(){return 24}static createBlock(t,e,n,r){return t.prep(8,24),t.writeInt64(BigInt(r!==null&&r!==void 0?r:0)),t.pad(4),t.writeInt32(n),t.writeInt64(BigInt(e!==null&&e!==void 0?e:0)),t.offset()}}class rn{constructor(){this.bb=null,this.bb_pos=0}__init(t,e){return this.bb_pos=t,this.bb=e,this}static getRootAsFooter(t,e){return(e||new rn).__init(t.readInt32(t.position())+t.position(),t)}static getSizePrefixedRootAsFooter(t,e){return t.setPosition(t.position()+U),(e||new rn).__init(t.readInt32(t.position())+t.position(),t)}version(){let t=this.bb.__offset(this.bb_pos,4);return t?this.bb.readInt16(this.bb_pos+t):Tt.V1}schema(t){let e=this.bb.__offset(this.bb_pos,6);return e?(t||new Kt).__init(this.bb.__indirect(this.bb_pos+e),this.bb):null}dictionaries(t,e){let n=this.bb.__offset(this.bb_pos,8);return n?(e||new Ma).__init(this.bb.__vector(this.bb_pos+n)+t*24,this.bb):null}dictionariesLength(){let t=this.bb.__offset(this.bb_pos,8);return t?this.bb.__vector_len(this.bb_pos+t):0}recordBatches(t,e){let n=this.bb.__offset(this.bb_pos,10);return n?(e||new Ma).__init(this.bb.__vector(this.bb_pos+n)+t*24,this.bb):null}recordBatchesLength(){let t=this.bb.__offset(this.bb_pos,10);return t?this.bb.__vector_len(this.bb_pos+t):0}customMetadata(t,e){let n=this.bb.__offset(this.bb_pos,12);return n?(e||new Nt).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos+n)+t*4),this.bb):null}customMetadataLength(){let t=this.bb.__offset(this.bb_pos,12);return t?this.bb.__vector_len(this.bb_pos+t):0}static startFooter(t){t.startObject(5)}static addVersion(t,e){t.addFieldInt16(0,e,Tt.V1)}static addSchema(t,e){t.addFieldOffset(1,e,0)}static addDictionaries(t,e){t.addFieldOffset(2,e,0)}static startDictionariesVector(t,e){t.startVector(24,e,8)}static addRecordBatches(t,e){t.addFieldOffset(3,e,0)}static startRecordBatchesVector(t,e){t.startVector(24,e,8)}static addCustomMetadata(t,e){t.addFieldOffset(4,e,0)}static createCustomMetadataVector(t,e){t.startVector(4,e.length,4);for(let n=e.length-1;n>=0;n--)t.addOffset(e[n]);return t.endVector()}static startCustomMetadataVector(t,e){t.startVector(4,e,4)}static endFooter(t){return t.endObject()}static finishFooterBuffer(t,e){t.finish(e)}static finishSizePrefixedFooterBuffer(t,e){t.finish(e,void 0,!0)}}var Hm=b(()=>{ht();wa();Js();pf()});function Fc(t,e){return new Map([...t||new Map,...e||new Map])}function Mf(t,e=new Map){for(let n=-1,r=t.length;++n0)Mf(o.children,e)}return e}var Ym,Km,P,N;var $e=b(()=>{ne();Ft();Ym=Symbol.for("apache-arrow/Schema"),Km=Symbol.for("apache-arrow/Field");P=class P{static isSchema(t){return(t===null||t===void 0?void 0:t[Ym])===!0}constructor(t=[],e,n,r=Tt.V5){if(this.fields=t||[],this.metadata=e||new Map,!n)n=Mf(this.fields);this.dictionaries=n,this.metadataVersion=r}get[Symbol.toStringTag](){return"Schema"}get names(){return this.fields.map((t)=>t.name)}toString(){return`Schema<{ ${this.fields.map((t,e)=>`${e}: ${t}`).join(", ")} }>`}select(t){let e=new Set(t),n=this.fields.filter((r)=>e.has(r.name));return new P(n,this.metadata)}selectAt(t){let e=t.map((n)=>this.fields[n]).filter(Boolean);return new P(e,this.metadata)}assign(...t){let e=t[0]instanceof P?t[0]:Array.isArray(t[0])?new P(t[0]):new P(t),n=[...this.fields],r=Fc(Fc(new Map,this.metadata),e.metadata),a=e.fields.filter((s)=>{let c=n.findIndex((l)=>l.name===s.name);return~c?(n[c]=s.clone({metadata:Fc(Fc(new Map,n[c].metadata),s.metadata)}))&&!1:!0}),o=Mf(a,new Map);return new P([...n,...a],r,new Map([...this.dictionaries,...o]))}};P.prototype.fields=null;P.prototype.metadata=null;P.prototype.dictionaries=null;P.prototype[Ym]=!0;Object.defineProperty(P,Symbol.hasInstance,{value:function(e){return Function.prototype[Symbol.hasInstance].call(this,e)||this===P&&P.isSchema(e)}});N=class N{static isField(t){return(t===null||t===void 0?void 0:t[Km])===!0}static new(...t){let[e,n,r,a]=t;if(t[0]&&typeof t[0]==="object")({name:e}=t[0]),n===void 0&&(n=t[0].type),r===void 0&&(r=t[0].nullable),a===void 0&&(a=t[0].metadata);return new N(`${e}`,n,r,a)}constructor(t,e,n=!1,r){this.name=t,this.type=e,this.nullable=n,this.metadata=r||new Map}get typeId(){return this.type.typeId}get[Symbol.toStringTag](){return"Field"}toString(){return`${this.name}: ${this.type}`}clone(...t){let[e,n,r,a]=t;return!t[0]||typeof t[0]!=="object"?[e=this.name,n=this.type,r=this.nullable,a=this.metadata]=t:{name:e=this.name,type:n=this.type,nullable:r=this.nullable,metadata:a=this.metadata}=t[0],N.new(e,n,r,a)}};N.prototype.type=null;N.prototype.name=null;N.prototype.nullable=null;N.prototype.metadata=null;N.prototype[Km]=!0;Object.defineProperty(N,Symbol.hasInstance,{value:function(e){return Function.prototype[Symbol.hasInstance].call(this,e)||this===N&&N.isField(e)}})});class Ii{static decode(t){t=new Bx(L(t));let e=rn.getRootAsFooter(t),n=P.decode(e.schema(),new Map,e.version());return new Gm(n,e)}static encode(t){let e=new Dx,n=P.encode(e,t.schema);rn.startRecordBatchesVector(e,t.numRecordBatches);for(let o of[...t.recordBatches()].slice().reverse())Kr.encode(e,o);let r=e.endVector();rn.startDictionariesVector(e,t.numDictionaries);for(let o of[...t.dictionaryBatches()].slice().reverse())Kr.encode(e,o);let a=e.endVector();return rn.startFooter(e),rn.addSchema(e,n),rn.addVersion(e,Tt.V5),rn.addRecordBatches(e,r),rn.addDictionaries(e,a),rn.finishFooterBuffer(e,rn.endFooter(e)),e.asUint8Array()}get numRecordBatches(){return this._recordBatches.length}get numDictionaries(){return this._dictionaryBatches.length}constructor(t,e=Tt.V5,n,r){this.schema=t,this.version=e,n&&(this._recordBatches=n),r&&(this._dictionaryBatches=r)}*recordBatches(){for(let t,e=-1,n=this.numRecordBatches;++e=0&&t=0&&t{Hm();ht();$e();ne();oe();Dx=ji,Bx=Ie;Gm=class Gm extends Ii{get numRecordBatches(){return this._footer.recordBatchesLength()}get numDictionaries(){return this._footer.dictionariesLength()}constructor(t,e){super(t,e.version());this._footer=e}getRecordBatch(t){if(t>=0&&t=0&&t=0;n--)t.addOffset(e[n]);return t.endVector()}static startCustomMetadataVector(t,e){t.startVector(4,e,4)}static endMessage(t){return t.endObject()}static finishMessageBuffer(t,e){t.finish(e)}static finishSizePrefixedMessageBuffer(t,e){t.finish(e,void 0,!0)}static createMessage(t,e,n,r,a,o){return Wt.startMessage(t),Wt.addVersion(t,e),Wt.addHeaderType(t,n),Wt.addHeader(t,r),Wt.addBodyLength(t,a),Wt.addCustomMetadata(t,o),Wt.endMessage(t)}}var Wm=b(()=>{ht();wa();hf();Js()});var Qm,Uc;var Xm=b(()=>{ir();hh();ec();af();ch();lh();fh();uh();yh();_h();dh();tf();Zu();cf();lf();of();ef();ph();mh();uf();Ju();nf();rf();sf();Qm=class Qm extends K{visit(t,e){return t==null||e==null?void 0:super.visit(t,e)}visitNull(t,e){return mi.startNull(e),mi.endNull(e)}visitInt(t,e){return Ue.startInt(e),Ue.addBitWidth(e,t.bitWidth),Ue.addIsSigned(e,t.isSigned),Ue.endInt(e)}visitFloat(t,e){return Nn.startFloatingPoint(e),Nn.addPrecision(e,t.precision),Nn.endFloatingPoint(e)}visitBinary(t,e){return li.startBinary(e),li.endBinary(e)}visitBinaryView(t,e){return ui.startBinaryView(e),ui.endBinaryView(e)}visitLargeBinary(t,e){return di.startLargeBinary(e),di.endLargeBinary(e)}visitBool(t,e){return fi.startBool(e),fi.endBool(e)}visitUtf8(t,e){return _i.startUtf8(e),_i.endUtf8(e)}visitUtf8View(t,e){return gi.startUtf8View(e),gi.endUtf8View(e)}visitLargeUtf8(t,e){return pi.startLargeUtf8(e),pi.endLargeUtf8(e)}visitDecimal(t,e){return Me.startDecimal(e),Me.addScale(e,t.scale),Me.addPrecision(e,t.precision),Me.addBitWidth(e,t.bitWidth),Me.endDecimal(e)}visitDate(t,e){return Bn.startDate(e),Bn.addUnit(e,t.unit),Bn.endDate(e)}visitTime(t,e){return Je.startTime(e),Je.addUnit(e,t.unit),Je.addBitWidth(e,t.bitWidth),Je.endTime(e)}visitTimestamp(t,e){let n=t.timezone&&e.createString(t.timezone)||void 0;if(Ze.startTimestamp(e),Ze.addUnit(e,t.unit),n!==void 0)Ze.addTimezone(e,n);return Ze.endTimestamp(e)}visitInterval(t,e){return Fn.startInterval(e),Fn.addUnit(e,t.unit),Fn.endInterval(e)}visitDuration(t,e){return Cn.startDuration(e),Cn.addUnit(e,t.unit),Cn.endDuration(e)}visitList(t,e){return hi.startList(e),hi.endList(e)}visitStruct(t,e){return yi.startStruct_(e),yi.endStruct_(e)}visitUnion(t,e){Le.startTypeIdsVector(e,t.typeIds.length);let n=Le.createTypeIdsVector(e,t.typeIds);return Le.startUnion(e),Le.addMode(e,t.mode),Le.addTypeIds(e,n),Le.endUnion(e)}visitDictionary(t,e){let n=this.visit(t.indices,e);if(Xn.startDictionaryEncoding(e),Xn.addId(e,BigInt(t.id)),Xn.addIsOrdered(e,t.isOrdered),n!==void 0)Xn.addIndexType(e,n);return Xn.endDictionaryEncoding(e)}visitFixedSizeBinary(t,e){return Rn.startFixedSizeBinary(e),Rn.addByteWidth(e,t.byteWidth),Rn.endFixedSizeBinary(e)}visitFixedSizeList(t,e){return On.startFixedSizeList(e),On.addListSize(e,t.listSize),On.endFixedSizeList(e)}visitMap(t,e){return Un.startMap(e),Un.addKeysSorted(e,t.keysSorted),Un.endMap(e)}};Uc=new Qm});function ey(t,e=new Map){return new P(Cx(t,e),Mc(t.metadata),e)}function Pf(t){return new le(t.count,ry(t.columns),iy(t.columns),null,ay(t.columns))}function ny(t){return new He(Pf(t.data),t.id,t.isDelta)}function Cx(t,e){return(t.fields||[]).filter(Boolean).map((n)=>N.fromJSON(n,e))}function Jm(t,e){return(t.children||[]).filter(Boolean).map((n)=>N.fromJSON(n,e))}function ry(t){return(t||[]).reduce((e,n)=>[...e,new or(n.count,Rx(n.VALIDITY)),...ry(n.children)],[])}function iy(t,e=[]){for(let n=-1,r=(t||[]).length;++ne+ +(n===0),0)}function ay(t){return(t||[]).reduce((e,n)=>[...e,...n.VARIADIC_DATA_BUFFERS?[n.VARIADIC_DATA_BUFFERS.length]:[],...ay(n.children)],[])}function oy(t,e){let n,r,a,o,s,c;if(!e||!(o=t.dictionary))s=ty(t,Jm(t,e)),a=new N(t.name,s,t.nullable,Mc(t.metadata));else if(!e.has(n=o.id))r=(r=o.indexType)?Zm(r):new Ee,e.set(n,s=ty(t,Jm(t,e))),c=new be(s,r,n,o.isOrdered),a=new N(t.name,c,t.nullable,Mc(t.metadata));else r=(r=o.indexType)?Zm(r):new Ee,c=new be(e.get(n),r,n,o.isOrdered),a=new N(t.name,c,t.nullable,Mc(t.metadata));return a||null}function Mc(t=[]){return new Map(t.map(({key:e,value:n})=>[e,n]))}function Zm(t){return new se(t.isSigned,t.bitWidth)}function ty(t,e){let n=t.type.name;switch(n){case"NONE":return new zt;case"null":return new zt;case"binary":return new ze;case"largebinary":return new Ln;case"binaryview":return new ct;case"utf8":return new Gt;case"largeutf8":return new Pn;case"utf8view":return new hn;case"bool":return new jt;case"list":return new ve((e||[])[0]);case"struct":return new it(e||[]);case"struct_":return new it(e||[])}switch(n){case"int":{let r=t.type;return new se(r.isSigned,r.bitWidth)}case"floatingpoint":{let r=t.type;return new Mn(Rt[r.precision])}case"decimal":{let r=t.type;return new kn(r.scale,r.precision,r.bitWidth)}case"date":{let r=t.type;return new tr(vt[r.unit])}case"time":{let r=t.type;return new er(E[r.unit],r.bitWidth)}case"timestamp":{let r=t.type;return new en(E[r.unit],r.timezone)}case"interval":{let r=t.type;return new nr(It[r.unit])}case"duration":{let r=t.type;return new rr(E[r.unit])}case"union":{let r=t.type,[a,...o]=(r.mode+"").toLowerCase(),s=a.toUpperCase()+o.join("");return new mn(yt[s],r.typeIds||[],e||[])}case"fixedsizebinary":{let r=t.type;return new Vn(r.byteWidth)}case"fixedsizelist":{let r=t.type;return new yn(r.listSize,(e||[])[0])}case"map":{let r=t.type;return new nn((e||[])[0],r.keysSorted)}}throw Error(`Unrecognized type: "${n}"`)}var sy=b(()=>{$e();Ft();Xi();ne()});class we{static fromJSON(t,e){let n=new we(0,Tt.V5,e);return n._createHeader=Fx(t,e),n}static decode(t){t=new Nx(L(t));let e=Wt.getRootAsMessage(t),n=e.bodyLength(),r=e.version(),a=e.headerType(),o=Yx(e),s=new we(n,r,a,void 0,o);return s._createHeader=Ux(e,a),s}static encode(t){let e=new Ox,n=-1;if(t.isSchema())n=P.encode(e,t.header());else if(t.isRecordBatch())n=le.encode(e,t.header());else if(t.isDictionaryBatch())n=He.encode(e,t.header());let r=!(t.metadata&&t.metadata.size>0)?-1:Wt.createCustomMetadataVector(e,[...t.metadata].map(([a,o])=>{let s=e.createString(`${a}`),c=e.createString(`${o}`);return Nt.startKeyValue(e),Nt.addKey(e,s),Nt.addValue(e,c),Nt.endKeyValue(e)}));if(Wt.startMessage(e),Wt.addVersion(e,Tt.V5),Wt.addHeader(e,n),Wt.addHeaderType(e,t.headerType),Wt.addBodyLength(e,BigInt(t.bodyLength)),r!==-1)Wt.addCustomMetadata(e,r);return Wt.finishMessageBuffer(e,Wt.endMessage(e)),e.asUint8Array()}static from(t,e=0){if(t instanceof P)return new we(0,Tt.V5,pt.Schema,t);if(t instanceof le)return new we(e,Tt.V5,pt.RecordBatch,t,t.metadata);if(t instanceof He)return new we(e,Tt.V5,pt.DictionaryBatch,t);throw Error(`Unrecognized Message header: ${t}`)}get type(){return this.headerType}get version(){return this._version}get headerType(){return this._headerType}get compression(){return this._compression}get bodyLength(){return this._bodyLength}get metadata(){return this._metadata}header(){return this._createHeader()}isSchema(){return this.headerType===pt.Schema}isRecordBatch(){return this.headerType===pt.RecordBatch}isDictionaryBatch(){return this.headerType===pt.DictionaryBatch}constructor(t,e,n,r,a){this._version=e,this._headerType=n,this.body=new Uint8Array(0),this._compression=r===null||r===void 0?void 0:r.compression,r&&(this._createHeader=()=>r),this._bodyLength=gt(t),this._metadata=a||new Map}}class le{get nodes(){return this._nodes}get length(){return this._length}get buffers(){return this._buffers}get compression(){return this._compression}get variadicBufferCounts(){return this._variadicBufferCounts}get metadata(){return this._metadata}constructor(t,e,n,r,a=[],o){this._nodes=e,this._buffers=n,this._length=gt(t),this._compression=r,this._variadicBufferCounts=a,this._metadata=o||new Map}}class He{get id(){return this._id}get data(){return this._data}get isDelta(){return this._isDelta}get length(){return this.data.length}get nodes(){return this.data.nodes}get buffers(){return this.data.buffers}constructor(t,e,n=!1){this._data=t,this._isDelta=n,this._id=gt(e)}}class Qt{constructor(t,e){this.offset=gt(t),this.length=gt(e)}}class or{constructor(t,e){this.length=gt(t),this.nullCount=gt(e)}}class La{constructor(t,e=$i.BUFFER){this.type=t,this.method=e}}function Fx(t,e){return()=>{switch(e){case pt.Schema:return P.fromJSON(t);case pt.RecordBatch:return le.fromJSON(t);case pt.DictionaryBatch:return He.fromJSON(t)}throw Error(`Unrecognized Message type: { name: ${pt[e]}, type: ${e} }`)}}function Ux(t,e){return()=>{switch(e){case pt.Schema:return P.decode(t.header(new Kt),new Map,t.version());case pt.RecordBatch:return le.decode(t.header(new Te),t.version());case pt.DictionaryBatch:return He.decode(t.header(new wr),t.version())}throw Error(`Unrecognized Message type: { name: ${pt[e]}, type: ${e} }`)}}function Mx(t,e=new Map,n=Tt.V5){let r=$x(t,e);return new P(r,Lc(t),e,n)}function Lx(t,e=Tt.V5){return new le(t.length(),qx(t),zx(t,e),fy(t.compression()),jx(t))}function Px(t,e=Tt.V5){return new He(le.decode(t.data(),e),t.id(),t.isDelta())}function kx(t){return new Qt(t.offset(),t.length())}function Vx(t){return new or(t.length(),t.nullCount())}function qx(t){let e=[];for(let n,r=-1,a=-1,o=t.nodesLength();++rN.encode(t,o));Kt.startFieldsVector(t,n.length);let r=Kt.createFieldsVector(t,n),a=!(e.metadata&&e.metadata.size>0)?-1:Kt.createCustomMetadataVector(t,[...e.metadata].map(([o,s])=>{let c=t.createString(`${o}`),l=t.createString(`${s}`);return Nt.startKeyValue(t),Nt.addKey(t,c),Nt.addValue(t,l),Nt.endKeyValue(t)}));if(Kt.startSchema(t),Kt.addFields(t,r),Kt.addEndianness(t,Zx?ci.Little:ci.Big),a!==-1)Kt.addCustomMetadata(t,a);return Kt.endSchema(t)}function Gx(t,e){let n=-1,r=-1,a=-1,{type:o,typeId:s}=e;if(!S.isDictionary(o))r=Uc.visit(o,t);else s=o.dictionary.typeId,a=Uc.visit(o,t),r=Uc.visit(o.dictionary,t);let c=(o.children||[]).map((p)=>N.encode(t,p)),l=ge.createChildrenVector(t,c),f=!(e.metadata&&e.metadata.size>0)?-1:ge.createCustomMetadataVector(t,[...e.metadata].map(([p,_])=>{let g=t.createString(`${p}`),w=t.createString(`${_}`);return Nt.startKeyValue(t),Nt.addKey(t,g),Nt.addValue(t,w),Nt.endKeyValue(t)}));if(e.name)n=t.createString(e.name);if(ge.startField(t),ge.addType(t,r),ge.addTypeType(t,s),ge.addChildren(t,l),ge.addNullable(t,!!e.nullable),n!==-1)ge.addName(t,n);if(a!==-1)ge.addDictionary(t,a);if(f!==-1)ge.addCustomMetadata(t,f);return ge.endField(t)}function Wx(t,e){let n=e.nodes||[],r=e.buffers||[],a=e.variadicBufferCounts||[];Te.startNodesVector(t,n.length);for(let f of n.slice().reverse())or.encode(t,f);let o=t.endVector();Te.startBuffersVector(t,r.length);for(let f of r.slice().reverse())Qt.encode(t,f);let s=t.endVector(),c=null;if(e.compression!==null)c=dy(t,e.compression);let l=-1;if(a.length>0)l=Te.createVariadicBufferCountsVector(t,a.map(BigInt));if(Te.startRecordBatch(t),Te.addLength(t,BigInt(e.length)),Te.addNodes(t,o),Te.addBuffers(t,s),e.compression!==null&&c)Te.addCompression(t,c);if(l!==-1)Te.addVariadicBufferCounts(t,l);return Te.endRecordBatch(t)}function dy(t,e){return Xe.startBodyCompression(t),Xe.addCodec(t,e.type),Xe.addMethod(t,e.method),Xe.endBodyCompression(t)}function Qx(t,e){let n=le.encode(t,e.data);return wr.startDictionaryBatch(t),wr.addId(t,BigInt(e.id)),wr.addIsDelta(t,e.isDelta),wr.addData(t,n),wr.endDictionaryBatch(t)}function Xx(t,e){return vo.createFieldNode(t,BigInt(e.length),BigInt(e.nullCount))}function Jx(t,e){return To.createBuffer(t,BigInt(e.offset),BigInt(e.length))}var Ox,Nx,Zx;var Xi=b(()=>{ht();pf();ec();Qu();oh();df();ff();wa();Xu();af();tf();Zu();cf();lf();of();ef();uf();nf();rf();sf();Wm();Wu();Gu();$e();oe();ne();Xm();sy();Ft();Ox=ji,Nx=Ie;N.encode=Gx;N.decode=Hx;N.fromJSON=oy;P.encode=Kx;P.decode=Mx;P.fromJSON=ey;le.encode=Wx;le.decode=Lx;le.fromJSON=Pf;He.encode=Qx;He.decode=Px;He.fromJSON=ny;or.encode=Xx;or.decode=Vx;Qt.encode=Jx;Qt.decode=kx;La.encode=dy;La.decode=fy;Zx=(()=>{let t=new ArrayBuffer(2);return new DataView(t).setInt16(0,256,!0),new Int16Array(t)[0]===256})()});class Pc{constructor(t){this._json=t}get schema(){return this._json.schema}get batches(){return this._json.batches||[]}get dictionaries(){return this._json.dictionaries||[]}}class Pa{tee(){return this._getDOMStream().tee()}pipe(t,e){return this._getNodeStream().pipe(t,e)}pipeTo(t,e){return this._getDOMStream().pipeTo(t,e)}pipeThrough(t,e){return this._getDOMStream().pipeThrough(t,e)}_getDOMStream(){return this._DOMStream||(this._DOMStream=this.toDOMStream())}_getNodeStream(){return this._nodeStream||(this._nodeStream=this.toNodeStream())}}var Pt,kf;var Vo=b(()=>{un();wo();Pt=Object.freeze({done:!0,value:void 0});kf=class kf extends Pa{constructor(){super();this._values=[],this.resolvers=[],this._closedPromise=new Promise((t)=>this._closedPromiseResolve=t)}get closed(){return this._closedPromise}cancel(t){return R(this,void 0,void 0,function*(){yield this.return(t)})}write(t){if(this._ensureOpen())this.resolvers.length<=0?this._values.push(t):this.resolvers.shift().resolve({done:!1,value:t})}abort(t){if(this._closedPromiseResolve)this.resolvers.length<=0?this._error={error:t}:this.resolvers.shift().reject({done:!0,value:t})}close(){if(this._closedPromiseResolve){let{resolvers:t}=this;while(t.length>0)t.shift().resolve(Pt);this._closedPromiseResolve(),this._closedPromiseResolve=void 0}}[Symbol.asyncIterator](){return this}toDOMStream(t){return Ae.toDOMStream(this._closedPromiseResolve||this._error?this:this._values,t)}toNodeStream(t){return Ae.toNodeStream(this._closedPromiseResolve||this._error?this:this._values,t)}throw(t){return R(this,void 0,void 0,function*(){return yield this.abort(t),Pt})}return(t){return R(this,void 0,void 0,function*(){return yield this.close(),Pt})}read(t){return R(this,void 0,void 0,function*(){return(yield this.next(t,"read")).value})}peek(t){return R(this,void 0,void 0,function*(){return(yield this.next(t,"peek")).value})}next(...t){if(this._values.length>0)return Promise.resolve({done:!1,value:this._values.shift()});else if(this._error)return Promise.reject({done:!0,value:this._error.error});else if(!this._closedPromiseResolve)return Promise.resolve(Pt);else return new Promise((e,n)=>{this.resolvers.push({resolve:e,reject:n})})}_ensureOpen(){if(this._closedPromiseResolve)return!0;throw Error("AsyncQueue is closed")}}});class hy{constructor(t){this.source=t}cancel(t){this.return(t)}peek(t){return this.next(t,"peek").value}read(t){return this.next(t,"read").value}next(t,e="read"){return this.source.next({cmd:e,size:t})}throw(t){return Object.create(this.source.throw&&this.source.throw(t)||Pt)}return(t){return Object.create(this.source.return&&this.source.return(t)||Pt)}}class Ti{constructor(t){this.source=t,this._closedPromise=new Promise((e)=>this._closedPromiseResolve=e)}cancel(t){return R(this,void 0,void 0,function*(){yield this.return(t)})}get closed(){return this._closedPromise}read(t){return R(this,void 0,void 0,function*(){return(yield this.next(t,"read")).value})}peek(t){return R(this,void 0,void 0,function*(){return(yield this.next(t,"peek")).value})}next(t){return R(this,arguments,void 0,function*(e,n="read"){return yield this.source.next({cmd:n,size:e})})}throw(t){return R(this,void 0,void 0,function*(){let e=this.source.throw&&(yield this.source.throw(t))||Pt;return this._closedPromiseResolve&&this._closedPromiseResolve(),this._closedPromiseResolve=void 0,Object.create(e)})}return(t){return R(this,void 0,void 0,function*(){let e=this.source.return&&(yield this.source.return(t))||Pt;return this._closedPromiseResolve&&this._closedPromiseResolve(),this._closedPromiseResolve=void 0,Object.create(e)})}}var Gr,Wr,jn;var Ji=b(()=>{un();wo();Pr();Vo();oe();Gr=class Gr extends kf{write(t){if((t=L(t)).byteLength>0)return super.write(t)}toString(t=!1){return t?ga(this.toUint8Array(!0)):this.toUint8Array(!1).then(ga)}toUint8Array(t=!1){return t?dn(this._values)[0]:(()=>R(this,void 0,void 0,function*(){var e,n,r,a;let o=[],s=0;try{for(var c=!0,l=Kn(this),f;f=yield l.next(),e=f.done,!e;c=!0){a=f.value,c=!1;let p=a;o.push(p),s+=p.byteLength}}catch(p){n={error:p}}finally{try{if(!c&&!e&&(r=l.return))yield r.call(l)}finally{if(n)throw n.error}}return dn(o,s)[0]}))()}};Wr=class Wr{constructor(t){if(t)this.source=new hy(Ae.fromIterable(t))}[Symbol.iterator](){return this}next(t){return this.source.next(t)}throw(t){return this.source.throw(t)}return(t){return this.source.return(t)}peek(t){return this.source.peek(t)}read(t){return this.source.read(t)}};jn=class jn{constructor(t){if(t instanceof jn)this.source=t.source;else if(t instanceof Gr)this.source=new Ti(Ae.fromAsyncIterable(t));else if(Gs(t))this.source=new Ti(Ae.fromNodeStream(t));else if(go(t))this.source=new Ti(Ae.fromDOMStream(t));else if(Ys(t))this.source=new Ti(Ae.fromDOMStream(t.body));else if(Wn(t))this.source=new Ti(Ae.fromIterable(t));else if(Gn(t))this.source=new Ti(Ae.fromAsyncIterable(t));else if(En(t))this.source=new Ti(Ae.fromAsyncIterable(t))}[Symbol.asyncIterator](){return this}next(t){return this.source.next(t)}throw(t){return this.source.throw(t)}return(t){return this.source.return(t)}get closed(){return this.source.closed}cancel(t){return this.source.cancel(t)}peek(t){return this.source.peek(t)}read(t){return this.source.read(t)}}});var kc,Zi;var Vf=b(()=>{un();Ji();oe();kc=class kc extends Wr{constructor(t,e){super();this.position=0,this.buffer=L(t),this.size=e===void 0?this.buffer.byteLength:e}readInt32(t){let{buffer:e,byteOffset:n}=this.readAt(t,4);return new DataView(e,n).getInt32(0,!0)}seek(t){return this.position=Math.min(t,this.size),tR(this,void 0,void 0,function*(){this.size=(yield t.stat()).size,delete this._pending}))()}readInt32(t){return R(this,void 0,void 0,function*(){let{buffer:e,byteOffset:n}=yield this.readAt(t,4);return new DataView(e,n).getInt32(0,!0)})}seek(t){return R(this,void 0,void 0,function*(){return this._pending&&(yield this._pending),this.position=Math.min(t,this.size),tVc,Int128:()=>$n,Int64:()=>Pe,Uint64:()=>Bt});function ka(t){if(t<0)t=4294967295+t+1;return`0x${t.toString(16)}`}class Vc{constructor(t){this.buffer=t}high(){return this.buffer[1]}low(){return this.buffer[0]}_times(t){let e=new Uint32Array([this.buffer[1]>>>16,this.buffer[1]&65535,this.buffer[0]>>>16,this.buffer[0]&65535]),n=new Uint32Array([t.buffer[1]>>>16,t.buffer[1]&65535,t.buffer[0]>>>16,t.buffer[0]&65535]),r=e[3]*n[3];this.buffer[0]=r&65535;let a=r>>>16;return r=e[2]*n[3],a+=r,r=e[3]*n[2]>>>0,a+=r,this.buffer[0]+=a<<16,this.buffer[1]=a>>>0>>16,this.buffer[1]+=e[1]*n[3]+e[2]*n[2]+e[3]*n[1],this.buffer[1]+=e[0]*n[3]+e[1]*n[2]+e[2]*n[1]+e[3]*n[0]<<16,this}_plus(t){let e=this.buffer[0]+t.buffer[0]>>>0;if(this.buffer[1]+=t.buffer[1],e>>0)++this.buffer[1];this.buffer[0]=e}lessThan(t){return this.buffer[1]>>0,e[2]=this.buffer[2]+t.buffer[2]>>>0,e[1]=this.buffer[1]+t.buffer[1]>>>0,e[0]=this.buffer[0]+t.buffer[0]>>>0,e[0]>>0)++e[1];if(e[1]>>0)++e[2];if(e[2]>>0)++e[3];return this.buffer[3]=e[3],this.buffer[2]=e[2],this.buffer[1]=e[1],this.buffer[0]=e[0],this}hex(){return`${ka(this.buffer[3])} ${ka(this.buffer[2])} ${ka(this.buffer[1])} ${ka(this.buffer[0])}`}static multiply(t,e){return new $n(new Uint32Array(t.buffer)).times(e)}static add(t,e){return new $n(new Uint32Array(t.buffer)).plus(e)}static from(t,e=new Uint32Array(4)){return $n.fromString(typeof t==="string"?t:t.toString(),e)}static fromNumber(t,e=new Uint32Array(4)){return $n.fromString(t.toString(),e)}static fromString(t,e=new Uint32Array(4)){let n=t.startsWith("-"),r=t.length,a=new $n(e);for(let o=n?1:0;o{qf=[1,10,100,1000,1e4,1e5,1e6,1e7,1e8];Bt=class Bt extends Vc{times(t){return this._times(t),this}plus(t){return this._plus(t),this}static from(t,e=new Uint32Array(2)){return Bt.fromString(typeof t==="string"?t:t.toString(),e)}static fromNumber(t,e=new Uint32Array(2)){return Bt.fromString(t.toString(),e)}static fromString(t,e=new Uint32Array(2)){let n=t.length,r=new Bt(e);for(let a=0;a$f,toIntervalDayTimeObjects:()=>tS,toIntervalMonthDayNanoInt32Array:()=>Hf,toIntervalMonthDayNanoObjects:()=>eS});function $f(t){var e,n;let r=t.length,a=new Int32Array(r*2);for(let o=0,s=0;o>>0,a[s++]=Number(f>>BigInt(32))>>>0}else s+=2}return a}function tS(t){let e=t.length,n=Array(e/2);for(let r=0,a=0;r>>0);r[o++]={months:t[a],days:t[a+1],nanoseconds:e?`${s}`:s}}return r}function my(t){let e=new Uint8Array(t.length/2);for(let n=0;n>1]=Number.parseInt(t.slice(n,n+2),16);return e}function nS(t){return my(t.join(""))}function yy(t,e){let n=new Uint8Array(t.length*16),r=new DataView(n.buffer);for(let[a,o]of t.entries()){let s=a*16,c=o.SIZE;if(r.setInt32(s,c,!0),o.INLINED!==void 0){let l=e(o.INLINED);for(let f=0;f>1)]=Number.parseInt(l.slice(f,f+2),16);r.setInt32(s+8,o.BUFFER_INDEX,!0),r.setInt32(s+12,o.OFFSET,!0)}}return n}function rS(t){return yy(t,(e)=>{let n=new Uint8Array(e.length/2);for(let r=0;r>1]=Number.parseInt(e.slice(r,r+2),16);return n})}function iS(t){return yy(t,(e)=>new TextEncoder().encode(e))}var qo,Kf,Gf;var _y=b(()=>{zn();$e();Ft();ir();Na();Pr();jf();ne();oe();qo=class qo extends K{constructor(t,e,n,r,a=Tt.V5,o=[]){super();this.nodesIndex=-1,this.buffersIndex=-1,this.variadicBufferIndex=-1,this.bytes=t,this.nodes=e,this.buffers=n,this.dictionaries=r,this.metadataVersion=a,this.variadicBufferCounts=o}visit(t){return super.visit(t instanceof N?t.type:t)}visitNull(t,{length:e}=this.nextFieldNode()){return A({type:t,length:e})}visitBool(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),data:this.readData(t)})}visitInt(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),data:this.readData(t)})}visitFloat(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),data:this.readData(t)})}visitUtf8(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),valueOffsets:this.readOffsets(t),data:this.readData(t)})}visitLargeUtf8(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),valueOffsets:this.readOffsets(t),data:this.readData(t)})}visitUtf8View(t,{length:e,nullCount:n}=this.nextFieldNode()){let r=this.readNullBitmap(t,n),a=this.readData(t),o=this.readVariadicBuffers(this.nextVariadicBufferCount());return A({type:t,length:e,nullCount:n,nullBitmap:r,["views"]:a,["variadicBuffers"]:o})}visitBinary(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),valueOffsets:this.readOffsets(t),data:this.readData(t)})}visitLargeBinary(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),valueOffsets:this.readOffsets(t),data:this.readData(t)})}visitBinaryView(t,{length:e,nullCount:n}=this.nextFieldNode()){let r=this.readNullBitmap(t,n),a=this.readData(t),o=this.readVariadicBuffers(this.nextVariadicBufferCount());return A({type:t,length:e,nullCount:n,nullBitmap:r,["views"]:a,["variadicBuffers"]:o})}visitFixedSizeBinary(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),data:this.readData(t)})}visitDate(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),data:this.readData(t)})}visitTimestamp(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),data:this.readData(t)})}visitTime(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),data:this.readData(t)})}visitDecimal(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),data:this.readData(t)})}visitList(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),valueOffsets:this.readOffsets(t),child:this.visit(t.children[0])})}visitStruct(t,{length:e,nullCount:n}=this.nextFieldNode()){return A({type:t,length:e,nullCount:n,nullBitmap:this.readNullBitmap(t,n),children:this.visitMany(t.children)})}visitUnion(t,{length:e,nullCount:n}=this.nextFieldNode()){if(this.metadataVersion0&&this.readData(t,n)||new Uint8Array(0)}readOffsets(t,e){return this.readData(t,e)}readTypeIds(t,e){return this.readData(t,e)}readData(t,{length:e,offset:n}=this.nextBufferRange()){return this.bytes.subarray(n,n+e)}readVariadicBuffers(t){return Array.from({length:t},()=>this.readData(null))}nextVariadicBufferCount(){var t;return(t=this.variadicBufferCounts[++this.variadicBufferIndex])!==null&&t!==void 0?t:0}readDictionary(t){return this.dictionaries.get(t.id)}};Kf=class Kf extends qo{constructor(t,e,n,r,a,o=[]){super(new Uint8Array(0),e,n,r,a,o);this.sources=t}readNullBitmap(t,e,{offset:n}=this.nextBufferRange()){return e<=0?new Uint8Array(0):Wi(this.sources[n])}readOffsets(t,{offset:e}=this.nextBufferRange()){return ut(Uint8Array,ut(t.OffsetArrayType,this.sources[e]))}readTypeIds(t,{offset:e}=this.nextBufferRange()){return ut(Uint8Array,ut(t.ArrayType,this.sources[e]))}readData(t,{offset:e}=this.nextBufferRange()){let{sources:n}=this;if(S.isTimestamp(t))return ut(Uint8Array,Pe.convertArray(n[e]));else if((S.isInt(t)||S.isTime(t))&&t.bitWidth===64||S.isDuration(t))return ut(Uint8Array,Pe.convertArray(n[e]));else if(S.isDate(t)&&t.unit===vt.MILLISECOND)return ut(Uint8Array,Pe.convertArray(n[e]));else if(S.isDecimal(t))return ut(Uint8Array,$n.convertArray(n[e]));else if(S.isBinary(t)||S.isLargeBinary(t)||S.isFixedSizeBinary(t))return nS(n[e]);else if(S.isBinaryView(t))return rS(n[e]);else if(S.isUtf8View(t))return iS(n[e]);else if(S.isBool(t))return Wi(n[e]);else if(S.isUtf8(t)||S.isLargeUtf8(t))return fn(n[e].join(""));else if(S.isInterval(t))switch(t.unit){case It.DAY_TIME:return $f(n[e]);case It.MONTH_DAY_NANO:return Hf(n[e]);default:break}return ut(Uint8Array,ut(t.ArrayType,n[e].map((r)=>+r)))}readVariadicBuffers(t){let e=[];for(let n=0;n{Sr();$t();oe();zo=class zo extends xn{constructor(t){super(t);this._values=new wn(Uint8Array)}get byteLength(){let t=this._pendingLength+this.length*4;return this._offsets&&(t+=this._offsets.byteLength),this._values&&(t+=this._values.byteLength),this._nulls&&(t+=this._nulls.byteLength),t}setValue(t,e){return super.setValue(t,L(e))}_flushPending(t,e){let n=this._offsets,r=this._values.reserve(e).buffer,a=0;for(let[o,s]of t)if(s===void 0)n.set(o,0);else{let c=s.length;r.set(s,a),n.set(o,c),a+=c}}}});var jo;var Qf=b(()=>{oe();Sr();$t();jo=class jo extends xn{constructor(t){super(t);this._values=new wn(Uint8Array)}get byteLength(){let t=this._pendingLength+this.length*4;return this._offsets&&(t+=this._offsets.byteLength),this._values&&(t+=this._values.byteLength),this._nulls&&(t+=this._nulls.byteLength),t}setValue(t,e){return super.setValue(t,L(e))}_flushPending(t,e){let n=this._offsets,r=this._values.reserve(e).buffer,a=0;for(let[o,s]of t)if(s===void 0)n.set(o,BigInt(0));else{let c=s.length;r.set(s,a),n.set(o,BigInt(c)),a+=c}}}});var Xf;var gy=b(()=>{Sr();$t();Xf=class Xf extends Lt{constructor(t){super(t);this._values=new Po}setValue(t,e){this._values.set(t,+e)}}});var qa,qc,zc;var by=b(()=>{$t();bn();qa=class qa extends ce{};qa.prototype._setValue=If;qc=class qc extends qa{};qc.prototype._setValue=rc;zc=class zc extends qa{};zc.prototype._setValue=ic});var jc;var wy=b(()=>{$t();bn();jc=class jc extends ce{};jc.prototype._setValue=Ef});var Jf;var xy=b(()=>{Ft();$t();$c();Jf=class Jf extends Lt{constructor({type:t,nullValues:e,dictionaryHashFunction:n}){super({type:new be(t.dictionary,t.indices,t.id,t.isOrdered)});if(this._nulls=null,this._dictionaryOffset=0,this._keysToIndices=Object.create(null),this.indices=Qr({type:this.type.indices,nullValues:e}),this.dictionary=Qr({type:this.type.dictionary,nullValues:null}),typeof n==="function")this.valueToKey=n}get values(){return this.indices.values}get nullCount(){return this.indices.nullCount}get nullBitmap(){return this.indices.nullBitmap}get byteLength(){return this.indices.byteLength+this.dictionary.byteLength}get reservedLength(){return this.indices.reservedLength+this.dictionary.reservedLength}get reservedByteLength(){return this.indices.reservedByteLength+this.dictionary.reservedByteLength}isValid(t){return this.indices.isValid(t)}setValid(t,e){let n=this.indices;return e=n.setValid(t,e),this.length=n.length,e}setValue(t,e){let n=this._keysToIndices,r=this.valueToKey(e),a=n[r];if(a===void 0)n[r]=a=this._dictionaryOffset+this.dictionary.append(e).length-1;return this.indices.setValue(t,a)}flush(){let t=this.type,e=this._dictionary,n=this.dictionary.toVector(),r=this.indices.flush().clone(t);return r.dictionary=e?e.concat(n):n,this.finished||(this._dictionaryOffset+=n.length),this._dictionary=r.dictionary,this.clear(),r}finish(){return this.indices.finish(),this.dictionary.finish(),this._dictionaryOffset=0,this._keysToIndices=Object.create(null),super.finish()}clear(){return this.indices.clear(),this.dictionary.clear(),super.clear()}valueToKey(t){return typeof t==="string"?t:`${t}`}}});var Hc;var Sy=b(()=>{$t();bn();Hc=class Hc extends ce{};Hc.prototype._setValue=Af});var Zf;var Ay=b(()=>{$e();$t();Ft();Zf=class Zf extends Lt{setValue(t,e){let[n]=this.children,r=t*this.stride;for(let a=-1,o=this.stride;++a0)throw Error("FixedSizeListBuilder can only have one child.");let n=this.children.push(t);return this.type=new yn(this.type.listSize,new N(e,t.type,!0)),n}}});var za,td,ed,nd;var Iy=b(()=>{No();$t();za=class za extends ce{setValue(t,e){this._values.set(t,e)}};td=class td extends za{setValue(t,e){super.setValue(t,Oo(e))}};ed=class ed extends za{};nd=class nd extends za{}});var ta,Yc,Kc,Gc;var Ty=b(()=>{$t();bn();ta=class ta extends ce{};ta.prototype._setValue=Df;Yc=class Yc extends ta{};Yc.prototype._setValue=pc;Kc=class Kc extends ta{};Kc.prototype._setValue=hc;Gc=class Gc extends ta{};Gc.prototype._setValue=mc});var vi,Wc,Qc,Xc,Jc;var vy=b(()=>{$t();bn();vi=class vi extends ce{};vi.prototype._setValue=Bf;Wc=class Wc extends vi{};Wc.prototype._setValue=yc;Qc=class Qc extends vi{};Qc.prototype._setValue=_c;Xc=class Xc extends vi{};Xc.prototype._setValue=gc;Jc=class Jc extends vi{};Jc.prototype._setValue=bc});var sr,rd,id,ad,od,sd,cd,ld,ud;var Ey=b(()=>{$t();sr=class sr extends ce{setValue(t,e){this._values.set(t,e)}};rd=class rd extends sr{};id=class id extends sr{};ad=class ad extends sr{};od=class od extends sr{};sd=class sd extends sr{};cd=class cd extends sr{};ld=class ld extends sr{};ud=class ud extends sr{}});var fd;var Dy=b(()=>{$e();Ft();Sr();$t();fd=class fd extends xn{constructor(t){super(t);this._offsets=new ko(t.type)}addChild(t,e="0"){if(this.numChildren>0)throw Error("ListBuilder can only have one child.");return this.children[this.numChildren]=t,this.type=new ve(new N(e,t.type,!0)),this.numChildren-1}_flushPending(t){let e=this._offsets,[n]=this.children;for(let[r,a]of t)if(typeof a>"u")e.set(r,0);else{let o=a,s=o.length,c=e.set(r,s).buffer[r];for(let l=-1;++l{$e();Ft();$t();dd=class dd extends xn{set(t,e){return super.set(t,e)}setValue(t,e){let n=e instanceof Map?e:new Map(Object.entries(e)),r=this._pending||(this._pending=new Map),a=r.get(t);a&&(this._pendingLength-=a.size),this._pendingLength+=n.size,r.set(t,n)}addChild(t,e=`${this.numChildren}`){if(this.numChildren>0)throw Error("ListBuilder can only have one child.");return this.children[this.numChildren]=t,this.type=new nn(new N(e,t.type,!0),this.type.keysSorted),this.numChildren-1}_flushPending(t){let e=this._offsets,[n]=this.children;for(let[r,a]of t)if(a===void 0)e.set(r,0);else{let{[r]:o,[r+1]:s}=e.set(r,a.size).buffer;for(let c of a.entries())if(n.set(o,c),++o>=s)break}}}});var pd;var Cy=b(()=>{$t();pd=class pd extends Lt{setValue(t,e){}setValid(t,e){return this.length=Math.max(t+1,this.length),e}}});var hd;var Ry=b(()=>{$e();$t();Ft();hd=class hd extends Lt{setValue(t,e){let{children:n,type:r}=this;switch(Array.isArray(e)||e.constructor){case!0:return r.children.forEach((a,o)=>n[o].set(t,e[o]));case Map:return r.children.forEach((a,o)=>n[o].set(t,e.get(a.name)));default:return r.children.forEach((a,o)=>n[o].set(t,e[a.name]))}}setValid(t,e){if(!super.setValid(t,e))this.children.forEach((n)=>n.setValid(t,e));return e}addChild(t,e=`${this.numChildren}`){let n=this.children.push(t);return this.type=new it([...this.type.children,new N(e,t.type,!0)]),n}}});var Ei,Zc,tl,el,nl;var Oy=b(()=>{$t();bn();Ei=class Ei extends ce{};Ei.prototype._setValue=Tf;Zc=class Zc extends Ei{};Zc.prototype._setValue=ac;tl=class tl extends Ei{};tl.prototype._setValue=oc;el=class el extends Ei{};el.prototype._setValue=sc;nl=class nl extends Ei{};nl.prototype._setValue=cc});var Di,rl,il,al,ol;var Ny=b(()=>{$t();bn();Di=class Di extends ce{};Di.prototype._setValue=vf;rl=class rl extends Di{};rl.prototype._setValue=lc;il=class il extends Di{};il.prototype._setValue=uc;al=class al extends Di{};al.prototype._setValue=fc;ol=class ol extends Di{};ol.prototype._setValue=dc});var $o,md,yd;var Fy=b(()=>{$e();Sr();$t();Ft();$o=class $o extends Lt{constructor(t){super(t);if(this._typeIds=new Ai(Int8Array,0,1),typeof t.valueToChildTypeId==="function")this._valueToChildTypeId=t.valueToChildTypeId}get typeIdToChildIndex(){return this.type.typeIdToChildIndex}append(t,e){return this.set(this.length,t,e)}set(t,e,n){if(n===void 0)n=this._valueToChildTypeId(this,e,t);return this.setValue(t,e,n),this}setValue(t,e,n){this._typeIds.set(t,n);let r=this.type.typeIdToChildIndex[n],a=this.children[r];a===null||a===void 0||a.set(t,e),this.length=Math.max(t+1,this.length)}addChild(t,e=`${this.children.length}`){let n=this.children.push(t),{type:{children:r,mode:a,typeIds:o}}=this,s=[...r,new N(e,t.type)];return this.type=new mn(a,[...o,n],s),n}_valueToChildTypeId(t,e,n){throw Error("Cannot map UnionBuilder value to child typeId. Pass the `childTypeId` as the second argument to unionBuilder.append(), or supply a `valueToChildTypeId` function as part of the UnionBuilder constructor options.")}};md=class md extends $o{};yd=class yd extends $o{constructor(t){super(t);this._offsets=new Ai(Int32Array)}setValue(t,e,n){let r=this._typeIds.set(t,n).buffer[t],a=this.getChildAt(this.type.typeIdToChildIndex[r]),o=this._offsets.set(t,a.length).buffer[t];a===null||a===void 0||a.set(o,e),this.length=Math.max(t+1,this.length)}}});var sl;var Uy=b(()=>{Pr();Wf();Sr();$t();sl=class sl extends xn{constructor(t){super(t);this._values=new wn(Uint8Array)}get byteLength(){let t=this._pendingLength+this.length*4;return this._offsets&&(t+=this._offsets.byteLength),this._values&&(t+=this._values.byteLength),this._nulls&&(t+=this._nulls.byteLength),t}setValue(t,e){return super.setValue(t,fn(e))}_flushPending(t,e){}};sl.prototype._flushPending=zo.prototype._flushPending});var cl;var My=b(()=>{Pr();Sr();$t();Qf();cl=class cl extends xn{constructor(t){super(t);this._values=new wn(Uint8Array)}get byteLength(){let t=this._pendingLength+this.length*4;return this._offsets&&(t+=this._offsets.byteLength),this._values&&(t+=this._values.byteLength),this._nulls&&(t+=this._nulls.byteLength),t}setValue(t,e){return super.setValue(t,fn(e))}_flushPending(t,e){}};cl.prototype._flushPending=jo.prototype._flushPending});var Ho;var _d=b(()=>{Ft();$t();Sr();oe();zn();Ho=class Ho extends Lt{constructor(t){super(t);this._variadicBuffers=[],this._currentBuffer=null,this._currentBufferIndex=0,this._currentBufferOffset=0,this._bufferSize=33554432,this._views=new wn(Uint8Array)}get byteLength(){let t=0;this._views&&(t+=this._views.byteLength),this._nulls&&(t+=this._nulls.byteLength);for(let e of this._variadicBuffers)t+=e.byteLength;return this._currentBuffer&&(t+=this._currentBuffer.byteLength),t}setValue(t,e){return this.writeBinaryValue(t,this.encodeValue(e))}writeBinaryValue(t,e){let n=e.length,r=(t+1)*ct.ELEMENT_WIDTH,a=this._views.length;if(r>a)this._views.reserve(r-a);let o=this._views.buffer,s=t*ct.ELEMENT_WIDTH,c=new DataView(o.buffer,o.byteOffset+s,ct.ELEMENT_WIDTH);if(c.setInt32(ct.LENGTH_OFFSET,n,!0),n<=ct.INLINE_CAPACITY){o.set(e,s+ct.INLINE_OFFSET);for(let l=n;lthis._bufferSize){if(this._currentBuffer)this._variadicBuffers.push(this._currentBuffer.buffer.slice(0,this._currentBufferOffset));this._currentBuffer=new wn(Uint8Array),this._currentBufferIndex=this._variadicBuffers.length,this._currentBufferOffset=0}this._currentBuffer.reserve(n).buffer.set(e,this._currentBufferOffset),c.setInt32(ct.BUFFER_INDEX_OFFSET,this._currentBufferIndex,!0),c.setInt32(ct.BUFFER_OFFSET_OFFSET,this._currentBufferOffset,!0),this._currentBufferOffset+=n}return this}encodeValue(t){return L(t)}setValid(t,e){let n=(t+1)*ct.ELEMENT_WIDTH,r=this._views.length;if(n>r)this._views.reserve(n-r);let a=super.setValid(t,e);if(!a){let o=this._views.buffer,s=t*ct.ELEMENT_WIDTH;for(let c=0;c0)this._variadicBuffers.push(this._currentBuffer.buffer.slice(0,this._currentBufferOffset)),this._currentBuffer=null,this._currentBufferOffset=0;let o=r.flush(e*ct.ELEMENT_WIDTH),s=n>0?a.flush(e):void 0,c=this._variadicBuffers.slice();return this._variadicBuffers=[],this._currentBufferIndex=0,this.clear(),A({type:t,length:e,nullCount:n,nullBitmap:s,["views"]:o,["variadicBuffers"]:c})}finish(){return this.finished=!0,this}}});var gd;var Ly=b(()=>{_d();Pr();gd=class gd extends Ho{constructor(t){super(t)}setValue(t,e){return this.writeBinaryValue(t,fn(e))}}});var Py,ky;var Vy=b(()=>{ir();Wf();Qf();gy();by();wy();xy();Sy();Ay();Iy();Ty();vy();Ey();Dy();By();Cy();Ry();Oy();Ny();Fy();Uy();My();_d();Ly();Py=class Py extends K{visitNull(){return pd}visitBool(){return Xf}visitInt(){return sr}visitInt8(){return rd}visitInt16(){return id}visitInt32(){return ad}visitInt64(){return od}visitUint8(){return sd}visitUint16(){return cd}visitUint32(){return ld}visitUint64(){return ud}visitFloat(){return za}visitFloat16(){return td}visitFloat32(){return ed}visitFloat64(){return nd}visitUtf8(){return sl}visitLargeUtf8(){return cl}visitBinary(){return zo}visitLargeBinary(){return jo}visitFixedSizeBinary(){return Hc}visitDate(){return qa}visitDateDay(){return qc}visitDateMillisecond(){return zc}visitTimestamp(){return Ei}visitTimestampSecond(){return Zc}visitTimestampMillisecond(){return tl}visitTimestampMicrosecond(){return el}visitTimestampNanosecond(){return nl}visitTime(){return Di}visitTimeSecond(){return rl}visitTimeMillisecond(){return il}visitTimeMicrosecond(){return al}visitTimeNanosecond(){return ol}visitDecimal(){return jc}visitList(){return fd}visitStruct(){return hd}visitUnion(){return $o}visitDenseUnion(){return yd}visitSparseUnion(){return md}visitDictionary(){return Jf}visitInterval(){return ta}visitIntervalDayTime(){return Yc}visitIntervalYearMonth(){return Kc}visitIntervalMonthDayNano(){return Gc}visitDuration(){return vi}visitDurationSecond(){return Wc}visitDurationMillisecond(){return Qc}visitDurationMicrosecond(){return Xc}visitDurationNanosecond(){return Jc}visitFixedSizeList(){return Zf}visitMap(){return dd}visitBinaryView(){return Ho}visitUtf8View(){return gd}};ky=new Py});function Ye(t,e){return e instanceof t.constructor}function Xr(t,e){return t===e||Ye(t,e)}function Jr(t,e){return t===e||Ye(t,e)&&t.bitWidth===e.bitWidth&&t.isSigned===e.isSigned}function ll(t,e){return t===e||Ye(t,e)&&t.precision===e.precision}function aS(t,e){return t===e||Ye(t,e)&&t.byteWidth===e.byteWidth}function bd(t,e){return t===e||Ye(t,e)&&t.unit===e.unit}function Yo(t,e){return t===e||Ye(t,e)&&t.unit===e.unit&&t.timezone===e.timezone}function Ko(t,e){return t===e||Ye(t,e)&&t.unit===e.unit&&t.bitWidth===e.bitWidth}function oS(t,e){return t===e||Ye(t,e)&&t.children.length===e.children.length&&Ar.compareManyFields(t.children,e.children)}function sS(t,e){return t===e||Ye(t,e)&&t.children.length===e.children.length&&Ar.compareManyFields(t.children,e.children)}function wd(t,e){return t===e||Ye(t,e)&&t.mode===e.mode&&t.typeIds.every((n,r)=>n===e.typeIds[r])&&Ar.compareManyFields(t.children,e.children)}function cS(t,e){return t===e||Ye(t,e)&&t.id===e.id&&t.isOrdered===e.isOrdered&&Ar.visit(t.indices,e.indices)&&Ar.visit(t.dictionary,e.dictionary)}function ul(t,e){return t===e||Ye(t,e)&&t.unit===e.unit}function Go(t,e){return t===e||Ye(t,e)&&t.unit===e.unit}function lS(t,e){return t===e||Ye(t,e)&&t.listSize===e.listSize&&t.children.length===e.children.length&&Ar.compareManyFields(t.children,e.children)}function uS(t,e){return t===e||Ye(t,e)&&t.keysSorted===e.keysSorted&&t.children.length===e.children.length&&Ar.compareManyFields(t.children,e.children)}function ea(t,e){return Ar.compareSchemas(t,e)}function qy(t,e){return Ar.compareFields(t,e)}function fl(t,e){return Ar.visit(t,e)}var j,Ar;var Wo=b(()=>{ir();j=class j extends K{compareSchemas(t,e){return t===e||e instanceof t.constructor&&this.compareManyFields(t.fields,e.fields)}compareManyFields(t,e){return t===e||Array.isArray(t)&&Array.isArray(e)&&t.length===e.length&&t.every((n,r)=>this.compareFields(n,e[r]))}compareFields(t,e){return t===e||e instanceof t.constructor&&t.name===e.name&&t.nullable===e.nullable&&this.visit(t.type,e.type)}};j.prototype.visitNull=Xr;j.prototype.visitBool=Xr;j.prototype.visitInt=Jr;j.prototype.visitInt8=Jr;j.prototype.visitInt16=Jr;j.prototype.visitInt32=Jr;j.prototype.visitInt64=Jr;j.prototype.visitUint8=Jr;j.prototype.visitUint16=Jr;j.prototype.visitUint32=Jr;j.prototype.visitUint64=Jr;j.prototype.visitFloat=ll;j.prototype.visitFloat16=ll;j.prototype.visitFloat32=ll;j.prototype.visitFloat64=ll;j.prototype.visitUtf8=Xr;j.prototype.visitLargeUtf8=Xr;j.prototype.visitUtf8View=Xr;j.prototype.visitBinary=Xr;j.prototype.visitLargeBinary=Xr;j.prototype.visitBinaryView=Xr;j.prototype.visitFixedSizeBinary=aS;j.prototype.visitDate=bd;j.prototype.visitDateDay=bd;j.prototype.visitDateMillisecond=bd;j.prototype.visitTimestamp=Yo;j.prototype.visitTimestampSecond=Yo;j.prototype.visitTimestampMillisecond=Yo;j.prototype.visitTimestampMicrosecond=Yo;j.prototype.visitTimestampNanosecond=Yo;j.prototype.visitTime=Ko;j.prototype.visitTimeSecond=Ko;j.prototype.visitTimeMillisecond=Ko;j.prototype.visitTimeMicrosecond=Ko;j.prototype.visitTimeNanosecond=Ko;j.prototype.visitDecimal=Xr;j.prototype.visitList=oS;j.prototype.visitStruct=sS;j.prototype.visitUnion=wd;j.prototype.visitDenseUnion=wd;j.prototype.visitSparseUnion=wd;j.prototype.visitDictionary=cS;j.prototype.visitInterval=ul;j.prototype.visitIntervalDayTime=ul;j.prototype.visitIntervalYearMonth=ul;j.prototype.visitIntervalMonthDayNano=ul;j.prototype.visitDuration=Go;j.prototype.visitDurationSecond=Go;j.prototype.visitDurationMillisecond=Go;j.prototype.visitDurationMicrosecond=Go;j.prototype.visitDurationNanosecond=Go;j.prototype.visitFixedSizeList=lS;j.prototype.visitMap=uS;Ar=new j});function Qr(t){let e=t.type,n=new(ky.getVisitFn(e)())(t);if(e.children&&e.children.length>0){let r=t.children||[],a={nullValues:t.nullValues},o=Array.isArray(r)?(s,c)=>r[c]||a:({name:s})=>r[s]||a;for(let[s,c]of e.children.entries()){let{type:l}=c,f=o(c,s);n.children.push(Qr(Object.assign(Object.assign({},f),{type:l})))}}return n}function Ir(t,e){if(t instanceof tt||t instanceof O||t.type instanceof S)return xr(t);if(ArrayBuffer.isView(t)&&!e)return xr(t);if(ArrayBuffer.isView(t)&&e){let o=t instanceof BigInt64Array||t instanceof BigUint64Array,s=e.ArrayType===BigInt64Array||e.ArrayType===BigUint64Array;if(o&&!s)throw TypeError(`Cannot convert BigInt input to ${e}. BigInt arrays can only target BigInt-based types (e.g. Int64, Uint64).`);if(!o&&s)throw TypeError(`Cannot convert non-BigInt input to ${e}. ${e} requires BigInt values.`);if(S.isInt(e)||S.isFloat(e)){let c=t.constructor===e.ArrayType?t:new e.ArrayType(t);return xr({type:e,data:c,offset:0,length:c.length,nullCount:0})}}let n={type:e!==null&&e!==void 0?e:dl(t),nullValues:[null]},r=[...xd(n)(t)],a=r.length===1?r[0]:r.reduce((o,s)=>o.concat(s));if(S.isDictionary(a.type))return a.memoize();return a}function dl(t){if(t.length===0)return new zt;let e=0,n=0,r=0,a=0,o=0,s=0,c=0,l=0;for(let f of t){if(f==null){++e;continue}switch(typeof f){case"bigint":++s;continue;case"boolean":++c;continue;case"number":++a;continue;case"string":++o;continue;case"object":if(Array.isArray(f))++n;else if(Object.prototype.toString.call(f)==="[object Date]")++l;else++r;continue}throw TypeError("Unable to infer Vector type from input values, explicit type declaration expected.")}if(a+e===t.length)return new _n;else if(o+e===t.length)return new be(new Gt,new Ee);else if(s+e===t.length)return new De;else if(c+e===t.length)return new jt;else if(l+e===t.length)return new Co;else if(n+e===t.length){let f=t,p=dl(f[f.findIndex((_)=>_!=null)]);if(f.every((_)=>_==null||fl(p,dl(_))))return new ve(new N("",p,!0))}else if(r+e===t.length){let f=new Map;for(let p of t)for(let _ of Object.keys(p))if(!f.has(_)&&p[_]!=null)f.set(_,new N(_,dl([p[_]]),!0));return new it([...f.values()])}throw TypeError("Unable to infer Vector type from input values, explicit type declaration expected.")}function xd(t){let{["queueingStrategy"]:e="count"}=t,{["highWaterMark"]:n=e!=="bytes"?Number.POSITIVE_INFINITY:Math.pow(2,14)}=t,r=e!=="bytes"?"length":"byteLength";return function*(a){let o=0,s=Qr(t);for(let c of a)if(s.append(c)[r]>=n)++o&&(yield s.toVector());if(s.finish().length>0||o===0)yield s.toVector()}}var $c=b(()=>{$e();Ft();zn();je();Vy();Wo()});function pl(t,e){return fS(t,e.map((n)=>n.data.concat()))}function fS(t,e){let n=[...t.fields],r=[],a={numBatches:e.reduce((_,g)=>Math.max(_,g.length),0)},o=0,s=0,c=-1,l=e.length,f,p=[];while(a.numBatches-- >0){for(s=Number.POSITIVE_INFINITY,c=-1;++c0)r[o++]=A({type:new it(n),length:s,nullCount:0,children:p.slice()})}}return[t=t.assign(n),r.map((_)=>new rt(t,_))]}function dS(t,e,n,r,a){var o;let s=(e+63&-64)>>3;for(let c=-1,l=r.length;++c=e)if(p===e)n[c]=f;else n[c]=f.slice(0,e),a.numBatches=Math.max(a.numBatches,r[c].unshift(f.slice(e,p-e)));else{let _=t[c];t[c]=_.clone({nullable:!0}),n[c]=(o=f===null||f===void 0?void 0:f._changeLengthAndBackfillNullBitmap(e))!==null&&o!==void 0?o:A({type:_.type,length:e,nullCount:e,nullBitmap:new Uint8Array(s)})}}return n}var zy=b(()=>{zn();Ft();Bi()});var jy,$y,Xt;var hl=b(()=>{ne();zn();je();$e();Ft();Wo();zy();Cc();Yi();bn();Rc();Oc();Ra();Bi();$y=Symbol.for("apache-arrow/Table");Xt=class Xt{static isTable(t){return(t===null||t===void 0?void 0:t[$y])===!0}constructor(...t){var e,n;if(t.length===0)return this.batches=[],this.schema=new P([]),this._offsets=[0],this;let r,a;if(t[0]instanceof P)r=t.shift();if(t.at(-1)instanceof Uint32Array)a=t.pop();let o=(c)=>{if(c){if(c instanceof rt)return[c];else if(c instanceof Xt)return c.batches;else if(c instanceof tt){if(c.type instanceof it)return[new rt(new P(c.type.children),c)]}else if(Array.isArray(c))return c.flatMap((l)=>o(l));else if(typeof c[Symbol.iterator]==="function")return[...c].flatMap((l)=>o(l));else if(typeof c==="object"){let l=Object.keys(c),f=l.map((g)=>new O([c[g]])),p=r!==null&&r!==void 0?r:new P(l.map((g,w)=>new N(String(g),f[w].type,f[w].nullable))),[,_]=pl(p,f);return _.length===0?[new rt(c)]:_}}return[]},s=t.flatMap((c)=>o(c));if(r=(n=r!==null&&r!==void 0?r:(e=s[0])===null||e===void 0?void 0:e.schema)!==null&&n!==void 0?n:new P([]),!(r instanceof P))throw TypeError("Table constructor expects a [Schema, RecordBatch[]] pair.");for(let c of s){if(!(c instanceof rt))throw TypeError("Table constructor expects a [Schema, RecordBatch[]] pair.");if(!ea(r,c.schema))throw TypeError("Table and inner RecordBatch schemas must be equivalent.")}this.schema=r,this.batches=s,this._offsets=a!==null&&a!==void 0?a:vc(this.data)}get data(){return this.batches.map(({data:t})=>t)}get numCols(){return this.schema.fields.length}get numRows(){return this.data.reduce((t,e)=>t+e.length,0)}get nullCount(){if(this._nullCount===-1)this._nullCount=Tc(this.data);return this._nullCount}isValid(t){return!1}get(t){return null}at(t){return this.get(Ki(t,this.numRows))}set(t,e){return}indexOf(t,e){return-1}[Symbol.iterator](){if(this.batches.length>0)return Ua.visit(new O(this.data));return[][Symbol.iterator]()}toArray(){return[...this]}toString(){return`[ ${this.toArray().join(`, `)} -]`}concat(...$){let Z=this.schema,Q=this.data.concat($.flatMap(({data:X})=>X));return new v$(Z,Q.map((X)=>new D0(Z,X)))}slice($,Z){let Q=this.schema;[$,Z]=BY({length:this.numRows},$,Z);let X=v4(this.data,this._offsets,$,Z);return new v$(Q,X.map((J)=>new D0(Q,J)))}getChild($){return this.getChildAt(this.schema.fields.findIndex((Z)=>Z.name===$))}getChildAt($){if($>-1&&$Q.children[$]);if(Z.length===0){let{type:Q}=this.schema.fields[$],X=I({type:Q,length:0,nullCount:0});Z.push(X._changeLengthAndBackfillNullBitmap(this.numRows))}return new r(Z)}return null}setChild($,Z){var Q;return this.setChildAt((Q=this.schema.fields)===null||Q===void 0?void 0:Q.findIndex((X)=>X.name===$),Z)}setChildAt($,Z){let Q=this.schema,X=[...this.batches];if($>-1&&$this.getChildAt(H));[J[$],Y[$]]=[W,Z],[Q,X]=CG(Q,Y)}return new v$(Q,X)}select($){let Z=this.schema.fields.reduce((Q,X,J)=>Q.set(X.name,J),new Map);return this.selectAt($.map((Q)=>Z.get(Q)).filter((Q)=>Q>-1))}selectAt($){let Z=this.schema.selectAt($),Q=this.batches.map((X)=>X.selectAt($));return new v$(Z,Q)}assign($){let Z=this.schema.fields,[Q,X]=$.schema.fields.reduce((Y,G,H)=>{let[z,q]=Y,P=Z.findIndex((U)=>U.name===G.name);return~P?q[P]=H:z.push(H),Y},[[],[]]),J=this.schema.assign($.schema),W=[...Z.map((Y,G)=>[G,X[G]]).map(([Y,G])=>G===void 0?this.getChildAt(Y):$.getChildAt(G)),...Q.map((Y)=>$.getChildAt(Y))].filter(Boolean);return new v$(...CG(J,W))}};qF=Symbol.toStringTag;v$[qF]=(($)=>{return $.schema=null,$.batches=[],$._offsets=new Uint32Array([0]),$._nullCount=-1,$[Symbol.isConcatSpreadable]=!0,$[PF]=!0,$.isValid=KW(_Y),$.get=KW(p$.getVisitFn(A.Struct)),$.set=y4(DZ.getVisitFn(A.Struct)),$.indexOf=h4(Z6.getVisitFn(A.Struct)),"Table"})(v$.prototype);Object.defineProperty(v$,Symbol.hasInstance,{value:function(Z){return Function.prototype[Symbol.hasInstance].call(this,Z)||this===v$&&v$.isTable(Z)}})});function FF($,Z,Q=Z.reduce((X,J)=>Math.max(X,J.length),0)){var X;let J=[...$.fields],W=[...Z],Y=(Q+63&-64)>>3;for(let[G,H]of $.fields.entries()){let z=Z[G];if(!z||z.length!==Q)J[G]=H.clone({nullable:!0}),W[G]=(X=z===null||z===void 0?void 0:z._changeLengthAndBackfillNullBitmap(Q))!==null&&X!==void 0?X:I({type:H.type,length:Q,nullCount:Q,nullBitmap:new Uint8Array(Y)})}return[$.assign(J),I({type:new j0(J),length:Q,children:W})]}function MF($,Z,Q=new Map){var X,J;if(((X=$===null||$===void 0?void 0:$.length)!==null&&X!==void 0?X:0)>0&&($===null||$===void 0?void 0:$.length)===(Z===null||Z===void 0?void 0:Z.length))for(let W=-1,Y=$.length;++W{$8();jG();ZQ();vZ();H$();WW();aJ();EQ();m4();f4();UF=Symbol.for("apache-arrow/RecordBatch");D0=class D0{static isRecordBatch($){return($===null||$===void 0?void 0:$[UF])===!0}constructor(...$){switch($.length){case 3:case 2:{if([this.schema]=$,!(this.schema instanceof Q0))throw TypeError("RecordBatch constructor expects a [Schema, Data] pair.");if([,this.data=I({nullCount:0,type:new j0(this.schema.fields),children:this.schema.fields.map((Z)=>I({type:Z.type,nullCount:0}))}),this._metadata=new Map]=$,!(this.data instanceof A0))throw TypeError("RecordBatch constructor expects a [Schema, Data] pair.");[this.schema,this.data]=FF(this.schema,this.data.children,this.data.length);break}case 1:{let[Z]=$,{fields:Q,children:X,length:J}=Object.keys(Z).reduce((G,H,z)=>{return G.children[z]=Z[H],G.length=Math.max(G.length,Z[H].length),G.fields[z]=a.new({name:H,type:Z[H].type,nullable:!0}),G},{length:0,fields:[],children:[]}),W=new Q0(Q),Y=I({type:new j0(Q),length:J,children:X,nullCount:0});[this.schema,this.data]=FF(W,Y.children,J),this._metadata=new Map;break}default:throw TypeError("RecordBatch constructor expects an Object mapping names to child Data, or a [Schema, Data] pair.")}}get metadata(){return this._metadata}get dictionaries(){return this._dictionaries||(this._dictionaries=MF(this.schema.fields,this.data.children))}get numCols(){return this.schema.fields.length}get numRows(){return this.data.length}get nullCount(){return this.data.nullCount}isValid($){return this.data.getValid($)}get($){return p$.visit(this.data,$)}at($){return this.get(tJ($,this.numRows))}set($,Z){return DZ.visit(this.data,$,Z)}indexOf($,Z){return Z6.visit(this.data,$,Z)}[Symbol.iterator](){return HW.visit(new r([this.data]))}toArray(){return[...this]}concat(...$){return new v$(this.schema,[this,...$])}slice($,Z){let[Q]=new r([this.data]).slice($,Z).data;return new D0(this.schema,Q,this._metadata)}getChild($){var Z;return this.getChildAt((Z=this.schema.fields)===null||Z===void 0?void 0:Z.findIndex((Q)=>Q.name===$))}getChildAt($){if($>-1&&$X.name===$),Z)}setChildAt($,Z){let Q=this.schema,X=this.data;if($>-1&&$Y.name===J);if(~W)X[W]=this.data.children[W]}return new D0(Z,I({type:Q,length:this.numRows,children:X}),this._metadata)}selectAt($){let Z=this.schema.selectAt($),Q=$.map((J)=>this.data.children[J]).filter(Boolean),X=I({type:new j0(Z.fields),length:this.numRows,children:Q});return new D0(Z,X,this._metadata)}};NF=Symbol.toStringTag;D0[NF]=(($)=>{return $._nullCount=-1,$[Symbol.isConcatSpreadable]=!0,$[UF]=!0,"RecordBatch"})(D0.prototype);Object.defineProperty(D0,Symbol.hasInstance,{value:function(Z){return Function.prototype[Symbol.hasInstance].call(this,Z)||this===D0&&D0.isRecordBatch(Z)}});LW=class LW extends D0{constructor($,Z){let Q=$.fields.map((J)=>I({type:J.type})),X=I({type:new j0($.fields),length:0,nullCount:0,children:Q});super($,X,Z||new Map)}}});function wG($,Z=0){for(let Q=-1,X=AW.length;++Q`Expected ${T0[$]} Message in stream, but was null or length 0.`,i2=($)=>`Header pointer of flatbuffer-encoded ${T0[$]} Message is null or length 0.`,LF=($,Z)=>`Expected to read ${$} metadata bytes, but only read ${Z}.`,AF=($,Z)=>`Expected to read ${$} bytes for message body, but only read ${Z}.`,lY,VG,RG,EG=4,o2="ARROW1",AW,DW,r2,DF;var a2=O(()=>{MQ();u$();y0();X6();q2();s$();J6();xY();lY=class lY{constructor($){this.source=$ instanceof SX?$:new SX($)}[Symbol.iterator](){return this}next(){let $;if(($=this.readMetadataLength()).done)return L$;if($.value===-1&&($=this.readMetadataLength()).done)return L$;if(($=this.readMetadata($.value)).done)return L$;return $}throw($){return this.source.throw($)}return($){return this.source.return($)}readMessage($){let Z;if((Z=this.next()).done)return null;if($!=null&&Z.value.headerType!==$)throw Error(s2($));return Z.value}readMessageBody($){if($<=0)return new Uint8Array(0);let Z=$0(this.source.read($));if(Z.byteLength<$)throw Error(AF($,Z.byteLength));return Z.byteOffset%8===0&&Z.byteOffset+Z.byteLength<=Z.buffer.byteLength?Z:Z.slice()}readSchema($=!1){let Z=T0.Schema,Q=this.readMessage(Z),X=Q===null||Q===void 0?void 0:Q.header();if($&&!X)throw Error(i2(Z));return X}readMetadataLength(){let $=this.source.read(EG),Z=$&&new UZ($),Q=(Z===null||Z===void 0?void 0:Z.readInt32(0))||0;return{done:Q===0,value:Q}}readMetadata($){let Z=this.source.read($);if(!Z)return L$;if(Z.byteLength<$)throw Error(LF($,Z.byteLength));return{done:!1,value:KZ.decode(Z)}}};VG=class VG{constructor($,Z){this.source=$ instanceof Z8?$:e1($)?new W6($,Z):new Z8($)}[Symbol.asyncIterator](){return this}next(){return n(this,void 0,void 0,function*(){let $;if(($=yield this.readMetadataLength()).done)return L$;if($.value===-1&&($=yield this.readMetadataLength()).done)return L$;if(($=yield this.readMetadata($.value)).done)return L$;return $})}throw($){return n(this,void 0,void 0,function*(){return yield this.source.throw($)})}return($){return n(this,void 0,void 0,function*(){return yield this.source.return($)})}readMessage($){return n(this,void 0,void 0,function*(){let Z;if((Z=yield this.next()).done)return null;if($!=null&&Z.value.headerType!==$)throw Error(s2($));return Z.value})}readMessageBody($){return n(this,void 0,void 0,function*(){if($<=0)return new Uint8Array(0);let Z=$0(yield this.source.read($));if(Z.byteLength<$)throw Error(AF($,Z.byteLength));return Z.byteOffset%8===0&&Z.byteOffset+Z.byteLength<=Z.buffer.byteLength?Z:Z.slice()})}readSchema(){return n(this,arguments,void 0,function*($=!1){let Z=T0.Schema,Q=yield this.readMessage(Z),X=Q===null||Q===void 0?void 0:Q.header();if($&&!X)throw Error(i2(Z));return X})}readMetadataLength(){return n(this,void 0,void 0,function*(){let $=yield this.source.read(EG),Z=$&&new UZ($),Q=(Z===null||Z===void 0?void 0:Z.readInt32(0))||0;return{done:Q===0,value:Q}})}readMetadata($){return n(this,void 0,void 0,function*(){let Z=yield this.source.read($);if(!Z)return L$;if(Z.byteLength<$)throw Error(LF($,Z.byteLength));return{done:!1,value:KZ.decode(Z)}})}};RG=class RG extends lY{constructor($){super(new Uint8Array(0));this._schema=!1,this._body=[],this._batchIndex=0,this._dictionaryIndex=0,this._json=$ instanceof n4?$:new n4($)}next(){let{_json:$}=this;if(!this._schema)return this._schema=!0,{done:!1,value:KZ.fromJSON($.schema,T0.Schema)};if(this._dictionaryIndex<$.dictionaries.length){let Z=$.dictionaries[this._dictionaryIndex++];return this._body=Z.data.columns,{done:!1,value:KZ.fromJSON(Z,T0.DictionaryBatch)}}if(this._batchIndex<$.batches.length){let Z=$.batches[this._batchIndex++];return this._body=Z.columns,{done:!1,value:KZ.fromJSON(Z,T0.RecordBatch)}}return this._body=[],L$}readMessageBody($){return Z(this._body);function Z(Q){return(Q||[]).reduce((X,J)=>[...X,...J.VALIDITY&&[J.VALIDITY]||[],...J.TYPE_ID&&[J.TYPE_ID]||[],...J.OFFSET&&[J.OFFSET]||[],...J.DATA&&[J.DATA]||[],...J.VIEWS&&[J.VIEWS]||[],...J.VARIADIC_DATA_BUFFERS||[],...Z(J.children)],[])}}readMessage($){let Z;if((Z=this.next()).done)return null;if($!=null&&Z.value.headerType!==$)throw Error(s2($));return Z.value}readSchema(){let $=T0.Schema,Z=this.readMessage($),Q=Z===null||Z===void 0?void 0:Z.header();if(!Z||!Q)throw Error(i2($));return Q}};AW=new Uint8Array(o2.length);for(let $=0;$=this.MIN_HEADER_LENGTH}_hasValidMagicNumber($){return this.LZ4_FRAME_MAGIC.every((Z,Q)=>$[Q]===Z)}_hasValidVersion($){return($[4]&192)>>6===1}}class CF{constructor(){this.ZSTD_MAGIC=new Uint8Array([40,181,47,253]),this.MIN_HEADER_LENGTH=6}isValidCodecEncode($){let Z=new Uint8Array([1,2,3,4,5,6,7,8]),Q=$.encode(Z);return this._isValidCompressed(Q)}_isValidCompressed($){return this._hasMinimumLength($)&&this._hasValidMagicNumber($)}_hasMinimumLength($){return $.length>=this.MIN_HEADER_LENGTH}_hasValidMagicNumber($){return this.ZSTD_MAGIC.every((Z,Q)=>$[Q]===Z)}}var jF;var VF=O(()=>{AY();jF={[gZ.LZ4_FRAME]:new OF,[gZ.ZSTD]:new CF}});class RF{constructor(){this.registry={}}set($,Z){if((Z===null||Z===void 0?void 0:Z.encode)&&typeof Z.encode==="function"&&!jF[$].isValidCodecEncode(Z))throw Error(`Encoder for ${gZ[$]} is not valid.`);this.registry[$]=Z}get($){var Z;return((Z=this.registry)===null||Z===void 0?void 0:Z[$])||null}}var OW;var t2=O(()=>{AY();VF();OW=new RF});var SG=-1,dY=8;class e2{get numDictionaries(){return this._dictionaryIndex}get numRecordBatches(){return this._recordBatchIndex}constructor($=new Map){this.closed=!1,this.autoDestroy=!0,this._dictionaryIndex=0,this._recordBatchIndex=0,this.dictionaries=$}isSync(){return!1}isAsync(){return!1}isFile(){return!1}isStream(){return!1}reset($){return this._dictionaryIndex=0,this._recordBatchIndex=0,this.schema=$,this.dictionaries=new Map,this}_loadRecordBatch($,Z,Q){let X;if($.compression!=null){let W=OW.get($.compression.type);if((W===null||W===void 0?void 0:W.decode)&&typeof W.decode==="function"){let{decommpressedBody:Y,buffers:G}=this._decompressBuffers($,Z,W);X=this._loadCompressedVectors($,Y,this.schema.fields),$=new a$($.length,$.nodes,G,null)}else throw Error("Record batch is compressed but codec not found")}else X=this._loadVectors($,Z,this.schema.fields);let J=I({type:new j0(this.schema.fields),length:$.length,children:X});return new D0(this.schema,J,Q)}_loadDictionaryBatch($,Z){let{id:Q,isDelta:X}=$,{dictionaries:J,schema:W}=this,Y=J.get(Q),G=W.dictionaries.get(Q),H;if($.data.compression!=null){let z=OW.get($.data.compression.type);if((z===null||z===void 0?void 0:z.decode)&&typeof z.decode==="function"){let{decommpressedBody:q,buffers:P}=this._decompressBuffers($.data,Z,z);H=this._loadCompressedVectors($.data,q,[G]),$=new yZ(new a$($.data.length,$.data.nodes,P,null,$.data.variadicBufferCounts),Q,X)}else throw Error("Dictionary batch is compressed but codec not found")}else H=this._loadVectors($.data,Z,[G]);return(Y&&X?Y.concat(new r(H)):new r(H)).memoize()}_loadVectors($,Z,Q){return new vY(Z,$.nodes,$.buffers,this.dictionaries,this.schema.metadataVersion,$.variadicBufferCounts).visitMany(Q)}_loadCompressedVectors($,Z,Q){return new D2(Z,$.nodes,$.buffers,this.dictionaries,this.schema.metadataVersion,$.variadicBufferCounts).visitMany(Q)}_decompressBuffers($,Z,Q){let X=[],J=[],W=0;for(let{offset:Y,length:G}of $.buffers){if(G===0){X.push(new Uint8Array(0)),J.push(new x$(W,0));continue}let H=new UZ(Z.subarray(Y,Y+G)),z=c0(H.readInt64(0)),q=H.bytes().subarray(dY),P=z===SG?q:Q.decode(q);X.push(P);let U=(W+7&-8)-W;W+=U,J.push(new x$(W,P.length)),W+=P.length}return{decommpressedBody:X,buffers:J}}}function kF($,Z){return Z&&typeof Z.autoDestroy==="boolean"?Z.autoDestroy:$.autoDestroy}function*EF($){let Z=z$.from($);try{if(!Z.open({autoDestroy:!1}).closed)do yield Z;while(!Z.reset().open().closed)}finally{Z.cancel()}}function wF($){return UQ(this,arguments,function*(){let Q=yield V0(z$.from($));try{if(!(yield V0(Q.open({autoDestroy:!1}))).closed)do yield yield V0(Q);while(!(yield V0(Q.reset().open())).closed)}finally{yield V0(Q.cancel())}})}function yC($){return new _X(new BF($))}function hC($){let Z=$.peek(DW+7&-8);return Z&&Z.byteLength>=4?!wG(Z)?new _X(new nY($)):new H6(new $H($.read())):new _X(new nY(function*(){}()))}function bC($){return n(this,void 0,void 0,function*(){let Z=yield $.peek(DW+7&-8);return Z&&Z.byteLength>=4?!wG(Z)?new K6(new oY($)):new H6(new $H(yield $.read())):new K6(new oY(function(){return UQ(this,arguments,function*(){})}()))})}function mC($){return n(this,void 0,void 0,function*(){let{size:Z}=yield $.stat(),Q=new W6($,Z);if(Z>=DF&&wG(yield Q.readAt(0,DW+7&-8)))return new BG(new SF(Q));return new K6(new oY(Q))})}var z$,_X,K6,H6,BG,nY,oY,$H,SF,BF;var kG=O(()=>{MQ();$8();ZQ();H$();u$();K2();NY();X6();J6();q2();mP();DJ();xY();a2();t2();y0();z$=class z$ extends PW{constructor($){super();this._impl=$}get closed(){return this._impl.closed}get schema(){return this._impl.schema}get autoDestroy(){return this._impl.autoDestroy}get dictionaries(){return this._impl.dictionaries}get numDictionaries(){return this._impl.numDictionaries}get numRecordBatches(){return this._impl.numRecordBatches}get footer(){return this._impl.isFile()?this._impl.footer:null}isSync(){return this._impl.isSync()}isAsync(){return this._impl.isAsync()}isFile(){return this._impl.isFile()}isStream(){return this._impl.isStream()}next(){return this._impl.next()}throw($){return this._impl.throw($)}return($){return this._impl.return($)}cancel(){return this._impl.cancel()}reset($){return this._impl.reset($),this._DOMStream=void 0,this._nodeStream=void 0,this}open($){let Z=this._impl.open($);return N8(Z)?Z.then(()=>this):this}readRecordBatch($){return this._impl.isFile()?this._impl.readRecordBatch($):null}[Symbol.iterator](){return this._impl[Symbol.iterator]()}[Symbol.asyncIterator](){return this._impl[Symbol.asyncIterator]()}toDOMStream(){return NZ.toDOMStream(this.isSync()?{[Symbol.iterator]:()=>this}:{[Symbol.asyncIterator]:()=>this})}toNodeStream(){return NZ.toNodeStream(this.isSync()?{[Symbol.iterator]:()=>this}:{[Symbol.asyncIterator]:()=>this},{objectMode:!0})}static throughNode($){throw Error('"throughNode" not available in this environment')}static throughDOM($,Z){throw Error('"throughDOM" not available in this environment')}static from($){if($ instanceof z$)return $;else if(a1($))return yC($);else if(e1($))return mC($);else if(N8($))return(()=>n(this,void 0,void 0,function*(){return yield z$.from(yield $)}))();else if($4($)||PY($)||Q4($)||fQ($))return bC(new Z8($));return hC(new SX($))}static readAll($){if($ instanceof z$)return $.isSync()?EF($):wF($);else if(a1($)||ArrayBuffer.isView($)||U8($)||t1($))return EF($);return wF($)}};_X=class _X extends z${constructor($){super($);this._impl=$}readAll(){return[...this]}[Symbol.iterator](){return this._impl[Symbol.iterator]()}[Symbol.asyncIterator](){return UQ(this,arguments,function*(){yield V0(yield*d6(F8(this[Symbol.iterator]())))})}};K6=class K6 extends z${constructor($){super($);this._impl=$}readAll(){return n(this,void 0,void 0,function*(){var $,Z,Q,X;let J=[];try{for(var W=!0,Y=F8(this),G;G=yield Y.next(),$=G.done,!$;W=!0){X=G.value,W=!1;let H=X;J.push(H)}}catch(H){Z={error:H}}finally{try{if(!W&&!$&&(Q=Y.return))yield Q.call(Y)}finally{if(Z)throw Z.error}}return J})}[Symbol.iterator](){throw Error("AsyncRecordBatchStreamReader is not Iterable")}[Symbol.asyncIterator](){return this._impl[Symbol.asyncIterator]()}};H6=class H6 extends _X{constructor($){super($);this._impl=$}};BG=class BG extends K6{constructor($){super($);this._impl=$}};nY=class nY extends e2{constructor($,Z){super(Z);this._reader=!a1($)?new lY(this._handle=$):new RG(this._handle=$)}isSync(){return!0}isStream(){return!0}[Symbol.iterator](){return this}cancel(){if(!this.closed&&(this.closed=!0))this.reset()._reader.return(),this._reader=null,this.dictionaries=null}open($){if(!this.closed){if(this.autoDestroy=kF(this,$),!(this.schema||(this.schema=this._reader.readSchema())))this.cancel()}return this}throw($){if(!this.closed&&this.autoDestroy&&(this.closed=!0))return this.reset()._reader.throw($);return L$}return($){if(!this.closed&&this.autoDestroy&&(this.closed=!0))return this.reset()._reader.return($);return L$}next(){if(this.closed)return L$;let $,{_reader:Z}=this;while($=this._readNextMessageAndValidate())if($.isSchema())this.reset($.header());else if($.isRecordBatch()){this._recordBatchIndex++;let Q=$.header(),X=Z.readMessageBody($.bodyLength);return{done:!1,value:this._loadRecordBatch(Q,X,$.metadata)}}else if($.isDictionaryBatch()){this._dictionaryIndex++;let Q=$.header(),X=Z.readMessageBody($.bodyLength),J=this._loadDictionaryBatch(Q,X);this.dictionaries.set(Q.id,J)}if(this.schema&&this._recordBatchIndex===0)return this._recordBatchIndex++,{done:!1,value:new LW(this.schema)};return this.return()}_readNextMessageAndValidate($){return this._reader.readMessage($)}};oY=class oY extends e2{constructor($,Z){super(Z);this._reader=new VG(this._handle=$)}isAsync(){return!0}isStream(){return!0}[Symbol.asyncIterator](){return this}cancel(){return n(this,void 0,void 0,function*(){if(!this.closed&&(this.closed=!0))yield this.reset()._reader.return(),this._reader=null,this.dictionaries=null})}open($){return n(this,void 0,void 0,function*(){if(!this.closed){if(this.autoDestroy=kF(this,$),!(this.schema||(this.schema=yield this._reader.readSchema())))yield this.cancel()}return this})}throw($){return n(this,void 0,void 0,function*(){if(!this.closed&&this.autoDestroy&&(this.closed=!0))return yield this.reset()._reader.throw($);return L$})}return($){return n(this,void 0,void 0,function*(){if(!this.closed&&this.autoDestroy&&(this.closed=!0))return yield this.reset()._reader.return($);return L$})}next(){return n(this,void 0,void 0,function*(){if(this.closed)return L$;let $,{_reader:Z}=this;while($=yield this._readNextMessageAndValidate())if($.isSchema())yield this.reset($.header());else if($.isRecordBatch()){this._recordBatchIndex++;let Q=$.header(),X=yield Z.readMessageBody($.bodyLength);return{done:!1,value:this._loadRecordBatch(Q,X,$.metadata)}}else if($.isDictionaryBatch()){this._dictionaryIndex++;let Q=$.header(),X=yield Z.readMessageBody($.bodyLength),J=this._loadDictionaryBatch(Q,X);this.dictionaries.set(Q.id,J)}if(this.schema&&this._recordBatchIndex===0)return this._recordBatchIndex++,{done:!1,value:new LW(this.schema)};return yield this.return()})}_readNextMessageAndValidate($){return n(this,void 0,void 0,function*(){return yield this._reader.readMessage($)})}};$H=class $H extends nY{get footer(){return this._footer}get numDictionaries(){return this._footer?this._footer.numDictionaries:0}get numRecordBatches(){return this._footer?this._footer.numRecordBatches:0}constructor($,Z){super($ instanceof o4?$:new o4($),Z)}isSync(){return!0}isFile(){return!0}open($){if(!this.closed&&!this._footer){this.schema=(this._footer=this._readFooter()).schema;for(let Z of this._footer.dictionaryBatches())Z&&this._readDictionaryBatch(this._dictionaryIndex++)}return super.open($)}readRecordBatch($){var Z;if(this.closed)return null;if(!this._footer)this.open();let Q=(Z=this._footer)===null||Z===void 0?void 0:Z.getRecordBatch($);if(Q&&this._handle.seek(Q.offset)){let X=this._reader.readMessage(T0.RecordBatch);if(X===null||X===void 0?void 0:X.isRecordBatch()){let J=X.header(),W=this._reader.readMessageBody(X.bodyLength);return this._loadRecordBatch(J,W,X.metadata)}}return null}_readDictionaryBatch($){var Z;let Q=(Z=this._footer)===null||Z===void 0?void 0:Z.getDictionaryBatch($);if(Q&&this._handle.seek(Q.offset)){let X=this._reader.readMessage(T0.DictionaryBatch);if(X===null||X===void 0?void 0:X.isDictionaryBatch()){let J=X.header(),W=this._reader.readMessageBody(X.bodyLength),Y=this._loadDictionaryBatch(J,W);this.dictionaries.set(J.id,Y)}}}_readFooter(){let{_handle:$}=this,Z=$.size-r2,Q=$.readInt32(Z),X=$.readAt(Z-Q,Q);return NJ.decode(X)}_readNextMessageAndValidate($){var Z;if(!this._footer)this.open();if(this._footer&&this._recordBatchIndexsuper.open}});return n(this,void 0,void 0,function*(){if(!this.closed&&!this._footer){this.schema=(this._footer=yield this._readFooter()).schema;for(let Q of this._footer.dictionaryBatches())Q&&(yield this._readDictionaryBatch(this._dictionaryIndex++))}return yield Z.open.call(this,$)})}readRecordBatch($){return n(this,void 0,void 0,function*(){var Z;if(this.closed)return null;if(!this._footer)yield this.open();let Q=(Z=this._footer)===null||Z===void 0?void 0:Z.getRecordBatch($);if(Q&&(yield this._handle.seek(Q.offset))){let X=yield this._reader.readMessage(T0.RecordBatch);if(X===null||X===void 0?void 0:X.isRecordBatch()){let J=X.header(),W=yield this._reader.readMessageBody(X.bodyLength);return this._loadRecordBatch(J,W,X.metadata)}}return null})}_readDictionaryBatch($){return n(this,void 0,void 0,function*(){var Z;let Q=(Z=this._footer)===null||Z===void 0?void 0:Z.getDictionaryBatch($);if(Q&&(yield this._handle.seek(Q.offset))){let X=yield this._reader.readMessage(T0.DictionaryBatch);if(X===null||X===void 0?void 0:X.isDictionaryBatch()){let J=X.header(),W=yield this._reader.readMessageBody(X.bodyLength),Y=this._loadDictionaryBatch(J,W);this.dictionaries.set(J.id,Y)}}})}_readFooter(){return n(this,void 0,void 0,function*(){let{_handle:$}=this;$._pending&&(yield $._pending);let Z=$.size-r2,Q=yield $.readInt32(Z),X=yield $.readAt(Z-Q,Q);return NJ.decode(X)})}_readNextMessageAndValidate($){return n(this,void 0,void 0,function*(){if(!this._footer)yield this.open();if(this._footer&&this._recordBatchIndex{let P=Q.typeIds[q],U=G[P],M=H[P];return z.slice(U,Math.min(X,M))}))}return this}function uC($){let Z;if($.nullCount>=$.length)return BQ.call(this,new Uint8Array($.length+7>>3));else if((Z=$.values)instanceof Uint8Array)return BQ.call(this,eJ($.offset,$.length,Z));return BQ.call(this,$6($.values))}function TX($){return BQ.call(this,$.values.subarray(0,$.length*$.stride))}function IG($){let{length:Z,values:Q,valueOffsets:X}=$,J=c0(X[0]),W=c0(X[Z]),Y=Math.min(W-J,Q.byteLength-J);return BQ.call(this,J4(-J,Z+1,X)),BQ.call(this,Q.subarray(J,J+Y)),this}function IF($){let{offset:Z,length:Q,stride:X,values:J,variadicBuffers:W=[]}=$;if(!J)throw Error("BinaryView data is missing view buffer");let Y=Z*X,G=Y+Q*X;BQ.call(this,J.subarray(Y,G));for(let H of W)BQ.call(this,H);return this._variadicBufferCounts.push(W.length),this}function QH($){let{length:Z,valueOffsets:Q}=$;if(Q){let{[0]:X,[Z]:J}=Q;return BQ.call(this,J4(-X,Z+1,Q)),this.visit($.children[0].slice(X,J-X))}return this.visit($.children[0])}function ZH($){return this.visitMany($.type.children.map((Z,Q)=>$.children[Q]).filter(Boolean))[0]}var D$;var _F=O(()=>{ZQ();R8();u$();DJ();s$();GW();X6();H$();D$=class D$ extends H0{static assemble(...$){let Z=(X)=>X.flatMap((J)=>Array.isArray(J)?Z(J):J instanceof D0?J.data.children:J.data),Q=new D$;return Q.visitMany(Z($)),Q}constructor(){super();this._byteLength=0,this._nodes=[],this._buffers=[],this._bufferRegions=[],this._variadicBufferCounts=[]}visit($){if($ instanceof r)return this.visitMany($.data),this;let{type:Z}=$;if(!k.isDictionary(Z)){let{length:Q}=$;if(Q>2147483647)throw RangeError("Cannot write arrays larger than 2^31 - 1 in length");if(k.isUnion(Z))this.nodes.push(new w8(Q,0));else{let{nullCount:X}=$;if(!k.isNull(Z))BQ.call(this,X<=0?new Uint8Array(0):eJ($.offset,Q,$.nullBitmap));this.nodes.push(new w8(Q,X))}}return super.visit($)}visitNull($){return this}visitDictionary($){return this.visit($.clone($.type.indices))}get nodes(){return this._nodes}get buffers(){return this._buffers}get byteLength(){return this._byteLength}get bufferRegions(){return this._bufferRegions}get variadicBufferCounts(){return this._variadicBufferCounts}};D$.prototype.visitBool=uC;D$.prototype.visitInt=TX;D$.prototype.visitFloat=TX;D$.prototype.visitUtf8=IG;D$.prototype.visitLargeUtf8=IG;D$.prototype.visitUtf8View=IF;D$.prototype.visitBinary=IG;D$.prototype.visitLargeBinary=IG;D$.prototype.visitBinaryView=IF;D$.prototype.visitFixedSizeBinary=TX;D$.prototype.visitDate=TX;D$.prototype.visitTimestamp=TX;D$.prototype.visitTime=TX;D$.prototype.visitDecimal=TX;D$.prototype.visitList=QH;D$.prototype.visitStruct=ZH;D$.prototype.visitUnion=fC;D$.prototype.visitInterval=TX;D$.prototype.visitDuration=TX;D$.prototype.visitFixedSizeList=QH;D$.prototype.visitMap=QH});function XH($,Z){let Q=Z;if(Z instanceof v$)Q=Z.batches,$.reset(void 0,Z.schema);for(let X of Q)$.write(X);return $.finish()}function JH($,Z){return n(this,void 0,void 0,function*(){var Q,X,J,W,Y,G,H;try{for(Q=!0,X=F8(Z);J=yield X.next(),W=J.done,!W;Q=!0){H=J.value,Q=!1;let z=H;$.write(z)}}catch(z){Y={error:z}}finally{try{if(!Q&&!W&&(G=X.return))yield G.call(X)}finally{if(Y)throw Y.error}}return $.finish()})}var z6,XQ,CW;var WH=O(()=>{MQ();jG();a2();ZQ();X6();X6();K2();u$();cY();J6();_F();s$();DJ();xY();AY();t2();y0();z6=class z6 extends PW{static throughNode($){throw Error('"throughNode" not available in this environment')}static throughDOM($,Z){throw Error('"throughDOM" not available in this environment')}constructor($){super();if(this._position=0,this._started=!1,this._compression=null,this._sink=new wX,this._schema=null,this._dictionaryBlocks=[],this._recordBatchBlocks=[],this._seenDictionaries=new Map,this._dictionaryDeltaOffsets=new Map,TZ($)||($={autoDestroy:!0,writeLegacyIpcFormat:!1,compressionType:null}),this._autoDestroy=typeof $.autoDestroy==="boolean"?$.autoDestroy:!0,this._writeLegacyIpcFormat=typeof $.writeLegacyIpcFormat==="boolean"?$.writeLegacyIpcFormat:!1,$.compressionType!=null){if(this._writeLegacyIpcFormat)throw Error("Legacy IPC format does not support columnar compression. Use modern IPC format (writeLegacyIpcFormat=false).");if(Object.values(gZ).includes($.compressionType))this._compression=new qW($.compressionType);else{let Z=Object.values(gZ).filter((Q)=>typeof Q==="string");throw Error(`Unsupported compressionType: ${$.compressionType} Available types: ${Z.join(", ")}`)}}else this._compression=null}toString($=!1){return this._sink.toString($)}toUint8Array($=!1){return this._sink.toUint8Array($)}writeAll($){if(N8($))return $.then((Z)=>this.writeAll(Z));else if(fQ($))return JH(this,$);return XH(this,$)}get closed(){return this._sink.closed}[Symbol.asyncIterator](){return this._sink[Symbol.asyncIterator]()}toDOMStream($){return this._sink.toDOMStream($)}toNodeStream($){return this._sink.toNodeStream($)}close(){return this.reset()._sink.close()}abort($){return this.reset()._sink.abort($)}finish(){return this._autoDestroy?this.close():this.reset(this._sink,this._schema),this}reset($=this._sink,Z=null){if($===this._sink||$ instanceof wX)this._sink=$;else if(this._sink=new wX,$&&V9($))this.toDOMStream({type:"bytes"}).pipeTo($);else if($&&R9($))this.toNodeStream({objectMode:!1}).pipe($);if(this._started&&this._schema)this._writeFooter(this._schema);if(this._started=!1,this._dictionaryBlocks=[],this._recordBatchBlocks=[],this._seenDictionaries=new Map,this._dictionaryDeltaOffsets=new Map,!Z||!G6(Z,this._schema))if(Z==null)this._position=0,this._schema=null;else this._started=!0,this._schema=Z,this._writeSchema(Z);return this}write($){let Z=null;if(!this._sink)throw Error("RecordBatchWriter is closed");else if($==null)return this.finish()&&void 0;else if($ instanceof v$&&!(Z=$.schema))return this.finish()&&void 0;else if($ instanceof D0&&!(Z=$.schema))return this.finish()&&void 0;if(Z&&!G6(Z,this._schema)){if(this._started&&this._autoDestroy)return this.close();this.reset(this._sink,Z)}if($ instanceof D0){if(!($ instanceof LW))this._writeRecordBatch($)}else if($ instanceof v$)this.writeAll($.batches);else if(U8($))this.writeAll($)}_writeMessage($,Z=8){let Q=Z-1,X=KZ.encode($),J=X.byteLength,W=!this._writeLegacyIpcFormat?8:4,Y=J+W+Q&~Q,G=Y-J-W;if($.headerType===T0.RecordBatch)this._recordBatchBlocks.push(new EX(Y,$.bodyLength,this._position));else if($.headerType===T0.DictionaryBatch)this._dictionaryBlocks.push(new EX(Y,$.bodyLength,this._position));if(!this._writeLegacyIpcFormat)this._write(Int32Array.of(-1));if(this._write(Int32Array.of(Y-W)),J>0)this._write(X);return this._writePadding(G)}_write($){if(this._started){let Z=$0($);if(Z&&Z.byteLength>0)this._sink.write(Z),this._position+=Z.byteLength}return this}_writeSchema($){return this._writeMessage(KZ.from($))}_writeFooter($){return this._writeLegacyIpcFormat?this._write(Int32Array.of(0)):this._write(Int32Array.of(-1,0))}_writeMagic(){return this._write(AW)}_writePadding($){return $>0?this._write(new Uint8Array($)):this}_writeRecordBatch($){let{byteLength:Z,nodes:Q,bufferRegions:X,buffers:J,variadicBufferCounts:W}=this._assembleRecordBatch($),Y=new a$($.numRows,Q,X,this._compression,W,$.metadata),G=KZ.from(Y,Z);return this._writeDictionaries($)._writeMessage(G)._writeBodyBuffers(J)}_assembleRecordBatch($){let{byteLength:Z,nodes:Q,bufferRegions:X,buffers:J,variadicBufferCounts:W}=D$.assemble($);if(this._compression!=null)({byteLength:Z,bufferRegions:X,buffers:J}=this._compressBodyBuffers(J));return{byteLength:Z,nodes:Q,bufferRegions:X,buffers:J,variadicBufferCounts:W}}_compressBodyBuffers($){let Z=OW.get(this._compression.type);if(!(Z===null||Z===void 0?void 0:Z.encode)||typeof Z.encode!=="function")throw Error(`Codec for compression type "${gZ[this._compression.type]}" has invalid encode method`);let Q=0,X=[],J=[];for(let Y of $){let G=$0(Y);if(G.length===0){X.push(new Uint8Array(0),new Uint8Array(0)),J.push(new x$(Q,0));continue}let H=Z.encode(G),z=H.length0)this._writePadding(W)}return this}_writeDictionaries($){var Z,Q;for(let[X,J]of $.dictionaries){let W=(Z=J===null||J===void 0?void 0:J.data)!==null&&Z!==void 0?Z:[],Y=this._seenDictionaries.get(X),G=(Q=this._dictionaryDeltaOffsets.get(X))!==null&&Q!==void 0?Q:0;if(!Y||Y.data[0]!==W[0])for(let[H,z]of W.entries())this._writeDictionaryBatch(z,X,H>0);else if(GQ.writeAll(X));else if(fQ($))return JH(Q,$);return XH(Q,$)}};CW=class CW extends z6{static writeAll($,Z){let Q=new CW(Z);if(N8($))return $.then((X)=>Q.writeAll(X));else if(fQ($))return JH(Q,$);return XH(Q,$)}constructor($){super($);this._autoDestroy=!0,this._writeLegacyIpcFormat=!1}_writeSchema($){return this._writeMagic()._writePadding(2)}_writeDictionaryBatch($,Z,Q=!1){if(!Q&&this._seenDictionaries.has(Z))throw Error("The Arrow File format does not support replacement dictionaries. ");return super._writeDictionaryBatch($,Z,Q)}_writeFooter($){let Z=NJ.encode(new NJ($,i0.V5,this._recordBatchBlocks,this._dictionaryBlocks));return super._writeFooter($)._write(Z)._write(Int32Array.of(Z.byteLength))._writeMagic()}}});function TF($,Z){if(fQ($))return cC($,Z);if(U8($))return pC($,Z);throw Error("toDOMStream() must be called with an Iterable or AsyncIterable")}function pC($,Z){let Q=null,X=(Z===null||Z===void 0?void 0:Z.type)==="bytes"||!1,J=(Z===null||Z===void 0?void 0:Z.highWaterMark)||Math.pow(2,24);return new ReadableStream(Object.assign(Object.assign({},Z),{start(Y){W(Y,Q||(Q=$[Symbol.iterator]()))},pull(Y){Q?W(Y,Q):Y.close()},cancel(){(Q===null||Q===void 0?void 0:Q.return)&&Q.return(),Q=null}}),Object.assign({highWaterMark:X?J:void 0},Z));function W(Y,G){let H,z=null,q=Y.desiredSize||null;while(!(z=G.next(X?q:null)).done){if(ArrayBuffer.isView(z.value)&&(H=$0(z.value)))q!=null&&X&&(q=q-H.byteLength+1),z.value=H;if(Y.enqueue(z.value),q!=null&&--q<=0)return}Y.close()}}function cC($,Z){let Q=null,X=(Z===null||Z===void 0?void 0:Z.type)==="bytes"||!1,J=(Z===null||Z===void 0?void 0:Z.highWaterMark)||Math.pow(2,24);return new ReadableStream(Object.assign(Object.assign({},Z),{start(Y){return n(this,void 0,void 0,function*(){yield W(Y,Q||(Q=$[Symbol.asyncIterator]()))})},pull(Y){return n(this,void 0,void 0,function*(){Q?yield W(Y,Q):Y.close()})},cancel(){return n(this,void 0,void 0,function*(){(Q===null||Q===void 0?void 0:Q.return)&&(yield Q.return()),Q=null})}}),Object.assign({highWaterMark:X?J:void 0},Z));function W(Y,G){return n(this,void 0,void 0,function*(){let H,z=null,q=Y.desiredSize||null;while(!(z=yield G.next(X?q:null)).done){if(ArrayBuffer.isView(z.value)&&(H=$0(z.value)))q!=null&&X&&(q=q-H.byteLength+1),z.value=H;if(Y.enqueue(z.value),q!=null&&--q<=0)return}Y.close()})}}var gF=O(()=>{MQ();s$()});function yF($){return new hF($)}class hF{constructor($){this._numChunks=0,this._finished=!1,this._bufferedSize=0;let{["readableStrategy"]:Z,["writableStrategy"]:Q,["queueingStrategy"]:X="count"}=$,J=O9($,["readableStrategy","writableStrategy","queueingStrategy"]);this._controller=null,this._builder=BX(J),this._getSize=X!=="bytes"?xF:vF;let{["highWaterMark"]:W=X==="bytes"?Math.pow(2,14):1000}=Object.assign({},Z),{["highWaterMark"]:Y=X==="bytes"?Math.pow(2,14):1000}=Object.assign({},Q);this.readable=new ReadableStream({["cancel"]:()=>{this._builder.clear()},["pull"]:(G)=>{this._maybeFlush(this._builder,this._controller=G)},["start"]:(G)=>{this._maybeFlush(this._builder,this._controller=G)}},{highWaterMark:W,size:X!=="bytes"?xF:vF}),this.writable=new WritableStream({["abort"]:()=>{this._builder.clear()},["write"]:()=>{this._maybeFlush(this._builder,this._controller)},["close"]:()=>{this._maybeFlush(this._builder.finish(),this._controller)}},{highWaterMark:Y,size:(G)=>this._writeValueAndReturnChunkSize(G)})}_writeValueAndReturnChunkSize($){let Z=this._bufferedSize;return this._bufferedSize=this._getSize(this._builder.append($)),this._bufferedSize-Z}_maybeFlush($,Z){if(Z==null)return;if(this._bufferedSize>=Z.desiredSize)++this._numChunks&&this._enqueue(Z,$.toVector());if($.finished){if($.length>0||this._numChunks===0)++this._numChunks&&this._enqueue(Z,$.toVector());if(!this._finished&&(this._finished=!0))this._enqueue(Z,null)}}_enqueue($,Z){this._bufferedSize=0,this._controller=null,Z==null?$.close():$.enqueue(Z)}}var xF=($)=>{var Z;return(Z=$===null||$===void 0?void 0:$.length)!==null&&Z!==void 0?Z:0},vF=($)=>{var Z;return(Z=$===null||$===void 0?void 0:$.byteLength)!==null&&Z!==void 0?Z:0};var bF=O(()=>{MQ();t4()});function _G($,Z){let Q=new wX,X=null,J=new ReadableStream({cancel(){return n(this,void 0,void 0,function*(){yield Q.close()})},start(G){return n(this,void 0,void 0,function*(){yield Y(G,X||(X=yield W()))})},pull(G){return n(this,void 0,void 0,function*(){X?yield Y(G,X):G.close()})}});return{writable:new WritableStream(Q,Object.assign({highWaterMark:Math.pow(2,14)},$)),readable:J};function W(){return n(this,void 0,void 0,function*(){return yield(yield z$.from(Q)).open(Z)})}function Y(G,H){return n(this,void 0,void 0,function*(){let z=G.desiredSize,q=null;while(!(q=yield H.next()).done)if(G.enqueue(q.value),z!=null&&--z<=0)return;G.close()})}}var mF=O(()=>{MQ();J6();kG()});function TG($,Z){let Q=new this($),X=new Z8(Q),J=new ReadableStream({cancel(){return n(this,void 0,void 0,function*(){yield X.cancel()})},pull(Y){return n(this,void 0,void 0,function*(){yield W(Y)})},start(Y){return n(this,void 0,void 0,function*(){yield W(Y)})}},Object.assign({highWaterMark:Math.pow(2,14)},Z));return{writable:new WritableStream(Q,$),readable:J};function W(Y){return n(this,void 0,void 0,function*(){let G=null,H=Y.desiredSize;while(G=yield X.read(H||null))if(Y.enqueue(G),H!=null&&(H-=G.byteLength)<=0)return;Y.close()})}}var fF=O(()=>{MQ();J6()});var YH={};P8(YH,{isArrowVector:()=>dF,isArrowTable:()=>oF,isArrowSchema:()=>uF,isArrowRecordBatch:()=>nF,isArrowField:()=>pF,isArrowDataType:()=>cF,isArrowData:()=>lF});function uF($){return Q0.isSchema($)}function pF($){return a.isField($)}function cF($){return k.isDataType($)}function lF($){return A0.isData($)}function dF($){return r.isVector($)}function nF($){return D0.isRecordBatch($)}function oF($){return v$.isTable($)}var sF=O(()=>{vZ();H$();$8();ZQ();DJ();jG()});var Gj;var tF=O(()=>{u$();$8();H$();vZ();t4();kG();WH();DJ();dK();N2();GW();SY();s$();WW();sF();cY();Gj=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},lK),F2),J2),nK),qK),X2),fK),L2),YH),{compareSchemas:G6,compareFields:HF,compareTypes:DG})});var JQ=O(()=>{NY();R$();kG();WH();gF();bF();mF();fF();tF();NZ.toDOMStream=TF;M$.throughDOM=yF;z$.throughDOM=_G;H6.throughDOM=_G;_X.throughDOM=_G;z6.throughDOM=TG;CW.throughDOM=TG;XQ.throughDOM=TG});function Kj(){let $=import.meta.require??globalThis.require??null;if(!$)throw Error("FdSink requires Node.js or Bun (node:fs.writeSync). For other runtimes, supply a custom AccessLogSink that wraps console.log or your logger.");return $("node:fs").writeSync}class KH{fd;_writeSync=Kj();constructor($){this.fd=$}write($){let Z=new TextEncoder().encode($),Q=0;while(Q>>0;return Z>>>0}class gG{rate;threshold;constructor($){if(this.rate=$,!(typeof $==="number"&&Number.isFinite($)&&$>=0&&$<=1))throw RangeError(`access-log sample rate must be between 0.0 and 1.0, got ${$}`);this.threshold=$*4294967295}keep($,Z){if(this.rate>=1)return!0;if($.status==="error")return!0;if(Lj(Z)>this.threshold)return!1;return $.sample_rate=this.rate,!0}}class $N{capacity;writeRecord;queue=[];dropped=0;scheduled=!1;constructor($,Z){this.capacity=$,this.writeRecord=Z}enqueue($){if(this.queue.length>=this.capacity){this.dropped++;return}if(this.dropped)$.dropped_records=this.dropped,this.dropped=0;if(this.queue.push($),!this.scheduled)this.scheduled=!0,setTimeout(()=>{this.scheduled=!1,this.flush()},0)}flush(){while(this.queue.length>0){let $=this.queue.shift();this.writeRecord($)}}get droppedCount(){return this.dropped}}class PH{sink;serverVersion;level;maxRecordBytes;sampler;traceContext;redactor;queue;constructor($,Z={}){this.sink=$;let Q=typeof Z==="string"?{serverVersion:Z}:Z;this.serverVersion=Q.serverVersion??"",this.level=Q.level??"INFO",this.maxRecordBytes=Q.maxRecordBytes??1048576,this.sampler=new gG(Q.sampleRate??1),this.traceContext=Q.traceContext??qH,this.redactor=Q.redactor??zH,this.queue=Q.async?new $N(Q.queueSize??1e4,(X)=>this.write(X)):null}flush(){this.queue?.flush()}get droppedRecords(){return this.queue?.droppedCount??0}onDispatchStart($){return{startNs:process.hrtime.bigint()}}onDispatchEnd($,Z,Q,X){let J=$,W=J?qj(Number(process.hrtime.bigint()-J.startNs)/1e6):0,Y=X?"error":"ok",G=X?X.type??X.constructor.name:"",H=X?.message??"",z=Z.protocol??"",q={timestamp:Hj(),level:"INFO",logger:"vgi_rpc.access",message:`${z}.${Z.method} ${Y}`,server_id:Z.serverId,protocol:z,protocol_hash:Z.protocolHash??"",method:Z.method,method_type:Z.methodType,principal:Z.principal??"",auth_domain:Z.authDomain??"",authenticated:Z.authenticated??!1,remote_addr:Z.remoteAddr??"",duration_ms:W,status:Y,error_type:G};if(H)q.error_message=H;if(this.serverVersion)q.server_version=this.serverVersion;if(Z.protocolVersion)q.protocol_version=Z.protocolVersion;if(Z.requestId)q.request_id=Z.requestId;if(Z.httpStatus!==void 0)q.http_status=Z.httpStatus;let P=this.currentTrace();if(P)q.trace_id=P.traceId,q.span_id=P.spanId;if(Z.requestData&&Z.requestData.length>0){let M=zj(Z.requestData);if(this.level==="DEBUG")q.request_data=M;else q.original_request_bytes=M.length,q.truncated="payload_omitted"}if(Z.methodType==="stream")q.stream_id=Z.streamId??"00000000000000000000000000000000";if(Z.cancelled)q.cancelled=!0;if(Z.claims&&Object.keys(Z.claims).length>0){let M=this.redact(Z.claims);if(Object.keys(M).length>0)q.claims=M}if(Z.requestBytes!==void 0)q.request_bytes=Z.requestBytes;if(Z.externalizedBytes)q.externalized_bytes=Z.externalizedBytes;if(Q.inputBatches+Q.outputBatches+Q.inputRows+Q.outputRows+Q.inputBytes+Q.outputBytes!==0)q.input_batches=Q.inputBatches,q.output_batches=Q.outputBatches,q.input_rows=Q.inputRows,q.output_rows=Q.outputRows,q.input_bytes=Q.inputBytes,q.output_bytes=Q.outputBytes;let U=Z.streamId||Z.requestId||`${q.timestamp}:${Z.method}`;if(Z.deferral){Z.deferral.defer((M)=>{if(M!==void 0)q.response_bytes=M;this.emit(q,U)});return}this.emit(q,U)}redact($){try{return this.redactor($)}catch(Z){return console.warn("vgi-rpc access log: claim redactor threw; dropping claims from the record",Z),{}}}currentTrace(){try{return this.traceContext()}catch{return null}}emit($,Z){if(!this.sampler.keep($,Z))return;if(this.queue){this.queue.enqueue($);return}this.write($)}write($){try{this.sink.write(`${this.format($)} -`)}catch{}}format($){let Z=JSON.stringify($);if(this.maxRecordBytes<=0||GH(Z)<=this.maxRecordBytes)return Z;let Q=$.request_data;if(typeof Q==="string"){if($.original_request_bytes=Q.length,delete $.request_data,$.truncated=!0,Z=JSON.stringify($),GH(Z)<=this.maxRecordBytes)return Z}if($.claims!==void 0){if($.claims={},$.truncated=!0,Z=JSON.stringify($),GH(Z)<=this.maxRecordBytes)return Z}let X={timestamp:$.timestamp,level:"INFO",logger:"vgi_rpc.access",message:"record_too_large",server_id:$.server_id??"",protocol:$.protocol??"",protocol_hash:$.protocol_hash??"",method:$.method??"",method_type:$.method_type??"unary",principal:$.principal??"",auth_domain:$.auth_domain??"",authenticated:$.authenticated??!1,remote_addr:$.remote_addr??"",duration_ms:$.duration_ms??0,status:$.status??"ok",error_type:$.error_type??"",truncated:"record_too_large"};if($.method_type==="stream"&&typeof $.stream_id==="string")X.stream_id=$.stream_id;if(X.status==="error")X.error_message=typeof $.error_message==="string"&&$.error_message?$.error_message:"record_too_large";if(typeof $.dropped_records==="number")X.dropped_records=$.dropped_records;if(typeof $.sample_rate==="number")X.sample_rate=$.sample_rate;return JSON.stringify(X)}}var Pj,HH="[redacted]",Fj,Nj,Uj,Mj="@opentelemetry/api",jW=null;var ZN=O(()=>{Pj=new TextEncoder;Fj=/password|token|secret|key|authorization|email|phone|address|birthdate|gender|^name$|given_name|family_name|middle_name|nickname|preferred_username|picture|profile|website/i;Nj=/^[0-9a-f]{32}$/,Uj=/^[0-9a-f]{16}$/});function VW($){let Z=Aj.exec($);if(!Z)throw Error(`Invalid protocol version '${$}': expected canonical semver MAJOR.MINOR.PATCH with non-negative integers and no leading zeros (no prereleases or build metadata).`);return[Number(Z[1]),Number(Z[2]),Number(Z[3])]}var b,gX,xG="method_not_implemented",vG="session_lost",yG="server_draining",QN="protocol_version_mismatch",xX,Aj,XX,B8,sY;var HZ=O(()=>{b=class b extends Error{errorType;errorMessage;remoteTraceback;constructor($,Z,Q){super(`${$}: ${Z}`);this.errorType=$,this.errorMessage=Z,this.remoteTraceback=Q,this.name="RpcError"}};gX=class gX extends Error{constructor($){super($);this.name="VersionError"}};xX=class xX extends gX{static errorKind=QN;errorKind=QN;constructor($){super($);this.name="ProtocolVersionError"}};Aj=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;XX=class XX extends Error{static errorKind=xG;errorKind=xG;constructor($){super($);this.name="MethodNotImplementedError"}};B8=class B8 extends Error{static errorKind=vG;errorKind=vG;constructor($){super($);this.name="SessionLostError"}};sY=class sY extends Error{static errorKind=yG;errorKind=yG;constructor($){super($);this.name="ServerDrainingError"}}});class zZ{domain;authenticated;principal;claims;constructor($,Z,Q,X={}){this.domain=$,this.authenticated=Z,this.principal=Q,this.claims=X}static anonymous(){return new zZ("",!1,null)}requireAuthenticated(){if(!this.authenticated)throw new b("AuthenticationError","Authentication required","")}}var q6=O(()=>{HZ()});var JX=67108864;function FH($){if(!/^[1-9][0-9]*$/.test($))throw TypeError("must be a positive decimal integer");let Z=Number($);if(!Number.isSafeInteger(Z)||Z>XN)throw TypeError(`must not exceed ${XN}`);return Z}function bG($,Z){if($===void 0)return;if(!Number.isSafeInteger($)||$<=0)throw TypeError(`${Z} must be a positive safe integer`);return $}function iY($){let Z=FH($);if(Z<65536)throw TypeError("must be at least 65536");return Z}function WQ($,Z){let Q=bG($,Z);if(Q!==void 0&&Q<65536)throw TypeError(`${Z} must be at least 65536`);return Q}function t$(...$){let Z;for(let Q of $)if(Q!==void 0&&Q>0&&(Z===void 0||Q{XN=Number.MAX_SAFE_INTEGER});function mG($,Z){return $.get(Z)??$.get(Z.toLowerCase())}function GN($,Z){let Q=mG($,Z);if(Q==null)return[];return Q.split(",").map((X)=>X.trim()).filter((X)=>X.length>0)}function rY($,Z,Q=!1){let X=$.get(Z)??$.get(Z.toLowerCase());if(X==null)return null;return Q?iY(X):FH(X)}function fG($){let Q=($.get(JN)??$.get(JN.toLowerCase()))==="true",X=null,J=$.get("Cache-Control")??$.get("cache-control");if(J)for(let G of J.split(",")){let H=G.trim().toLowerCase();if(H.startsWith("max-age=")){let z=Number.parseFloat(H.slice(8));if(Number.isFinite(z))X=Date.now()+z*1000;break}}let Y=mG($,YN)==null?["zstd"]:GN($,YN).map((G)=>G.toLowerCase());return{maxRequestBytes:rY($,Dj),uploadUrlSupport:Q,maxUploadBytes:rY($,Oj),maxResponseBytes:rY($,Cj,!0),acceptMaxResponseBytesSupport:($.get(WN)??$.get(WN.toLowerCase()))==="true",maxExternalizedResponseBytes:rY($,jj,!0),externalizationEnabled:mG($,Vj)==="true",supportedEncodings:Y,stickyEnabled:mG($,Rj)==="true",stickyDefaultTtl:rY($,Ej),stickyEchoHeaders:GN($,wj),cacheExpiresAt:X}}function CJ($){let Z=fG($);if(!Z.acceptMaxResponseBytesSupport)throw new b("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true on every RPC response","");return Z}async function k8($,Z,Q,X,J=globalThis.fetch){let W={};if(Q)W.Authorization=Q;let Y=X??JX;WQ(Y,"acceptedMaxResponseBytes"),W[bZ]=String(Y);let G=await J(`${$}${Z}/health`,{method:"OPTIONS",headers:W});if(!G.ok)throw new b("TransportError",`Capability discovery failed: HTTP ${G.status}`,"");return fG(G.headers)}function uG($){if(!$)return!1;if($.cacheExpiresAt==null)return!0;return Date.now()<$.cacheExpiresAt}var Dj="VGI-Max-Request-Bytes",JN="VGI-Upload-URL-Support",Oj="VGI-Max-Upload-Bytes",Cj="VGI-Max-Response-Bytes",WN="VGI-Accept-Max-Response-Bytes-Support",jj="VGI-Max-Externalized-Response-Bytes",Vj="VGI-Externalization-Enabled",YN="VGI-Supported-Encodings",Rj="VGI-Sticky-Enabled",Ej="VGI-Sticky-Default-TTL",wj="VGI-Sticky-Echo-Headers";var RW=O(()=>{HZ();OJ()});function Sj($){return typeof $==="object"&&$!==null&&typeof $.buffers==="object"}function NH($,Z){let Q=-1;if(ArrayBuffer.isView($))Q=$.byteLength;else if($ instanceof ArrayBuffer)Q=$.byteLength;else if(Sj($))for(let X of Object.values($.buffers)){let J=X?.byteLength;if(typeof J==="number"&&J>Q)Q=J}else if(typeof $==="string")Q=$.length;if(Q>2147483640)throw RangeError(`${Z} is ${Q} bytes; this TypeScript worker can encode at most 2147483640 (2 GiB - 8). Both Arrow backends align buffers with (byteLength + 7) & ~7, and the bitwise & truncates to int32, so a larger value would be sent as a negative bodyLength. The wire and the `+"protocol carry it fine — the limit is the JavaScript Arrow encoders', not vgi-rpc's.")}function O0($,Z,Q=!0,X){return new a($,Z,Q,X??new Map)}function E0($,Z){return new Q0($,Z??new Map)}function eY($){let Z=new XQ;return Z.reset(void 0,$),Z.close(),Z.toUint8Array(!0)}function $1($){let Z=z$.from($),Q=[...Z];if(Q.length>0)return Q[0].schema;if(Z.schema)return Z.schema;throw Error("Cannot deserialize schema from empty IPC stream")}function mZ($){let Z=$,Q=new XQ;return Q.reset(void 0,Z.schema),Q._writeRecordBatch(Z),Q.close(),Q.toUint8Array(!0)}function F6($){let Z=new XQ;Z.reset(void 0,$);let Q=()=>{let X=Z._sink._values;if(X.length===1){let G=X[0];return X.length=0,G}let J=X.reduce((G,H)=>G+H.length,0),W=new Uint8Array(J),Y=0;for(let G of X)W.set(G,Y),Y+=G.length;return X.length=0,W};return{start:()=>Q(),writeBatch(X){return Z._writeRecordBatch(X),Q()},finish:()=>new Uint8Array(new Int32Array([-1,0]).buffer)}}function RH($){return[...z$.from($)].filter((Q)=>Q.constructor.name!=="_InternalEmptyPlaceholderRecordBatch")}function kQ($){let Z=z$.from($),Q=[...Z];if(Q.length===0){let X=Z.schema??new Q0([]),J=new j0(X.fields),W=I({type:J,length:0,children:[],nullCount:0});return new D0(X,W)}return Q[0]}function YQ($,Z){let Q=$,X=Q.fields.length>0?Z[Q.fields[0].name]?.length??0:0,J=Q.fields.map((G)=>{let H=Z[G.name];if(!H)return I({type:G.type,length:X,nullCount:X});for(let z of H)NH(z,G.name);return QX(H,G.type).data[0]}),W=new j0(Q.fields),Y=I({type:W,length:X,children:J,nullCount:0});return new D0(Q,Y)}function GQ($,Z){let Q=$,X=Q.fields.map((Y)=>EW(Y.type)),J=new j0(Q.fields),W=I({type:J,length:0,children:X,nullCount:0});return new D0(Q,W,Z)}function EW($){let Z={DataType:k,Data:A0};if(Z.DataType.isStruct($)){let Q=$.children.map((X)=>EW(X.type));return I({type:$,length:0,children:Q,nullCount:0})}if(Z.DataType.isList($)){let Q=EW($.children[0].type);return I({type:$,length:0,child:Q,nullCount:0,valueOffsets:new Int32Array([0])})}if(Z.DataType.isFixedSizeList($)){let Q=EW($.children[0].type);return I({type:$,length:0,child:Q,nullCount:0})}if(Z.DataType.isMap($)){let Q=$.children[0]?.type,X=Q?EW(Q):I({type:new j0([]),length:0,children:[],nullCount:0});return I({type:$,length:0,child:X,nullCount:0,valueOffsets:new Int32Array([0])})}if(Z.DataType.isUnion($)){let Q=$.children.map((X)=>EW(X.type));if(Z.DataType.isDenseUnion($))return I({type:$,length:0,typeIds:new Int8Array(0),valueOffsets:new Int32Array(0),children:Q,nullCount:0});return I({type:$,length:0,typeIds:new Int8Array(0),children:Q,nullCount:0})}return I({type:$,length:0,nullCount:0})}function vX($,Z,Q){let X=$,J={DataType:k,Data:A0},W=X.fields.map((H)=>{let z=Z[H.name];if(NH(z,H.name),z instanceof J.Data)return z;return QX([z],H.type).data[0]}),Y=new j0(X.fields),G=I({type:Y,length:1,children:W,nullCount:0});return new D0(X,G,Q)}function e$($,Z){let Q=$;return new D0(Q.schema,Q.data,Z)}function N6($,Z){let Q=new XQ;Q.reset(void 0,$);for(let X of Z)Q._writeRecordBatch(X);return Q.close(),Q.toUint8Array(!0)}function U6($,Z){let Q=$;if(Q.numRows===0)return $;let X=Z;if(Q.schema.fields.length!==X.fields.length)throw TypeError(`Field count mismatch: expected ${X.fields.length}, got ${Q.schema.fields.length}`);for(let H=0;H{let q=Q.data.children[z],P=q.type,U=H.type;if(P===U)return q;if(!Bj(P,U))return J=!0,q.clone(U);if(KN(P)&&KN(U)){J=!0;let M=Q.getChildAt(z),L=[];for(let D=0;Dnew V$,UH=()=>new s8,MH=()=>new i8,LH=()=>new GZ,VJ=()=>new AZ,AH=()=>new r8,DH=()=>new a8,OH=()=>new t8,CH=()=>new VQ,jH=()=>new e8,tY=()=>new $Q,Z$=()=>new T$,X8=()=>new xZ,VH=($=null)=>new tZ(l.MICROSECOND,$),P6=($)=>new LZ($),pG=($)=>new j0($),Bj=($,Z)=>{if($.typeId===Z.typeId)return!1;if($.constructor===Z.constructor)return!1;return!0},KN=($)=>$.typeId===A.Int||$.typeId===A.Float;var WX=O(()=>{JQ();aY={name:"arrow-js",opaquePassthrough:!0}});var yX="vgi_rpc.method",RJ="vgi_rpc.protocol",KQ="vgi_rpc.log_level",M6="vgi_rpc.log_message",EJ="vgi_rpc.log_extra",hX="vgi_rpc.request_version",IQ="1",wW="vgi_rpc.server_id",wJ="vgi_rpc.request_id",SJ="vgi_rpc.protocol_version",y$="vgi_rpc.stream_state#b64",YX="vgi_rpc.call_state#b64",bX="vgi_rpc.cancel",cG="vgi_rpc.location",EH="vgi_rpc.location.sha256",HN="vgi_rpc.location.fetch_ms",zN="vgi_rpc.location.source",BJ="X-VGI-RPC-Error",lG="X-Request-ID",dG="vgi_rpc.error_kind";function fH($){return $!=null&&typeof $.numRows==="number"&&$.schema!=null&&Array.isArray($.schema.fields)}var g0,Z1=($)=>$.typeId===g0.Int,wH=($)=>$.typeId===g0.Float,SH=($)=>$.typeId===g0.Binary||$.typeId===g0.LargeBinary,BH=($)=>$.typeId===g0.Utf8||$.typeId===g0.LargeUtf8,kH=($)=>$.typeId===g0.LargeUtf8,IH=($)=>$.typeId===g0.LargeBinary,_H=($)=>$.typeId===g0.Bool,TH=($)=>$.typeId===g0.Decimal,gH=($)=>$.typeId===g0.Date,xH=($)=>$.typeId===g0.Time,vH=($)=>$.typeId===g0.Timestamp,yH=($)=>$.typeId===g0.Duration,hH=($)=>$.typeId===g0.Map,bH=($)=>$.typeId===g0.FixedSizeBinary,mH=($)=>$.typeId===g0.Dictionary;var uH=O(()=>{g0={Null:1,Int:2,Float:3,Binary:4,Utf8:5,Bool:6,Decimal:7,Date:8,Time:9,Timestamp:10,Interval:11,List:12,Struct:13,Union:14,FixedSizeBinary:15,FixedSizeList:16,Map:17,Duration:18,LargeBinary:19,LargeUtf8:20,Dictionary:-1}});var qZ=O(()=>{WX();uH()});function O$($,Z){let Q=$.metadata;if(!Q)return!1;let X=Q.get(KQ);if(!X)return!1;let J=Q.get(M6)??"";if(X==="EXCEPTION"){let W=Q.get(EJ),Y="RpcError",G=J,H="";if(W)try{let z=JSON.parse(W);Y=z.exception_type??"RpcError",G=z.exception_message??J,H=z.traceback??""}catch{}throw new b(Y,G,H)}if(Z){let W=Q.get(EJ),Y;if(W)try{Y=JSON.parse(W)}catch{}Z({level:X,message:J,extra:Y})}return!0}var pH=O(()=>{HZ()});function FN($,Z){var Q=[],X=+!Z,J=0,W=0;for(;$.length;){var Y=vj($,X||Z);if(typeof Y=="object"){if(X){if(Z=null,Y.w.length==Y.u)Q.push(Z=Y.w),W+=Y.u}else Q.push(Z),Y.e=0;for(;!Y.l;){var G=cj($,Y,Z);if(!G)c$(5);if(Z)Y.e=Y.y;else Q.push(G),W+=G.length,Tj(Y.w,0,G.length),Y.w.set(G,Y.w.length-G.length)}J=Y.b+Y.c*4}else J=Y;$=$.subarray(J)}return lj(Q,W)}var Ij,h$,nG,_j,oG,cH=function($,Z,Q){if(h$.prototype.slice)return h$.prototype.slice.call($,Z,Q);if(Z==null||Z<0)Z=0;if(Q==null||Q>$.length)Q=$.length;var X=new h$(Q-Z);return X.set($.subarray(Z,Q)),X},X1=function($,Z,Q,X){if(h$.prototype.fill)return h$.prototype.fill.call($,Z,Q,X);if(Q==null||Q<0)Q=0;if(X==null||X>$.length)X=$.length;for(;Q$.length)X=$.length;while(Q>>0},vj=function($,Z){var Q=$[0]|$[1]<<8|$[2]<<16;if(Q==3126568&&$[3]==253){var X=$[4],J=X>>5&1,W=X>>2&1,Y=X&3,G=X>>6;if(X&8)c$(0);var H=6-J,z=Y==3?4:Y,q=qN($,H,z);H+=z;var P=G?1<>3);M=L+(L>>3)*($[5]&7)}if(M>2145386496)c$(1);var D=new h$((Z==1?U||M:Z?0:M)+12);return D[0]=1,D[4]=4,D[8]=8,{b:H+P,y:0,l:0,d:q,w:Z&&Z!=1?Z:D.subarray(12),e:M,o:new oG(D.buffer,0,3),u:U,c:W,m:Math.min(131072,M)}}else if((Q>>4|$[3]<<20)==25481893)return xj($,4)+8;c$(0)},L6=function($){var Z=0;for(;1<Q)c$(3);var W=1<0){var g=L6(Y+1),w=X>>3,T=(1<>(X&7)&T,_=(1<_)E-=y;if(U[++G]=--E,E==-1)Y+=E,R[--q]=G;else Y-=E;if(!E)do{var s=X>>3;H=($[s]|$[s+1]<<8)>>(X&7)&3,X+=2,G+=H}while(H==3)}if(G>255||Y)c$(0);var f=0,c=(W>>1)+(W>>3)+3,o=W-1;for(var e=0;e<=G;++e){var m=U[e];if(m<1){M[e]=-m;continue}for(z=0;z=q)}}if(f)c$(0);for(z=0;z>3,{b:J,s:R,n:V,t:L}]},yj=function($,Z){var Q=0,X=-1,J=new h$(292),W=$[Z],Y=J.subarray(0,256),G=J.subarray(256,268),H=new nG(J.buffer,268);if(W<128){var z=J1($,Z+1,6),q=z[0],P=z[1];Z+=W;var U=q<<3,M=$[Z];if(!M)c$(0);var L=0,D=0,R=P.b,V=R,g=(++Z<<3)-8+L6(M);for(;;){if(g-=R,g>3;if(L+=($[w]|$[w+1]<<8)>>(g&7)&(1<>3,D+=($[w]|$[w+1]<<8)>>(g&7)&(1<255)c$(0)}else{X=W-127;for(;Q>4,Y[Q+1]=T&15}++Z}var E=0;for(Q=0;Q11)c$(0);E+=_&&1<<_-1}var y=L6(E)+1,h=1<0;--Q){var e=H[Q];X1(o,Q,e,H[Q-1]=e+G[Q]*(1<G&&P>3,M=($[U]|$[U+1]<<8|$[U+2]<<16)>>(q&7);H=(H<>2,Y=W<<1,G=W+Y;Q1($.subarray(X,X+=$[0]|$[1]<<8),Z.subarray(0,W),Q),Q1($.subarray(X,X+=$[2]|$[3]<<8),Z.subarray(W,Y),Q),Q1($.subarray(X,X+=$[4]|$[5]<<8),Z.subarray(Y,G),Q),Q1($.subarray(X),Z.subarray(G),Q)},cj=function($,Z,Q){var X,J=Z.b,W=$[J],Y=W>>1&3;Z.l=W&1;var G=W>>3|$[J+1]<<5|$[J+2]<<13,H=(J+=3)+G;if(Y==1){if(J>=$.length)return;if(Z.b=J+1,Q)return X1(Q,$[J],Z.y,Z.y+=G),Q;return X1(new h$(G),$[J])}if(H>$.length)return;if(Y==0){if(Z.b=H,Q)return Q.set($.subarray(J,H),Z.y),Z.y+=G,Q;return cH($,J,H)}if(Y==2){var z=$[J],q=z&3,P=z>>2&3,U=z>>4,M=0,L=0;if(q<2)if(P&1)U|=$[++J]<<4|(P&2&&$[++J]<<12);else U=z>>3;else if(L=P,P<2)U|=($[++J]&63)<<4,M=$[J]>>6|$[++J]<<2;else if(P==2)U|=$[++J]<<4|($[++J]&3)<<12,M=$[J]>>2|$[++J]<<6;else U|=$[++J]<<4|($[++J]&63)<<12,M=$[J]>>6|$[++J]<<2|$[++J]<<10;++J;var D=Q?Q.subarray(Z.y,Z.y+Z.m):new h$(Z.m),R=D.length-U;if(q==0)D.set($.subarray(J,J+=U),R);else if(q==1)X1(D,$[J++],R);else{var V=Z.h;if(q==2){var g=yj($,J);M+=J-(J=g[0]),Z.h=V=g[1]}else if(!V)c$(0);(L?pj:Q1)($.subarray(J,J+=M),D.subarray(R),V)}var w=$[J++];if(w){if(w==255)w=($[J++]|$[J++]<<8)+32512;else if(w>127)w=w-128<<8|$[J++];var T=$[J++];if(T&3)c$(0);var E=[bj,mj,hj];for(var _=2;_>-1;--_){var y=T>>(_<<1)+2&3;if(y==1){var h=new h$([0,0,$[J++]]);E[_]={s:h.subarray(2,3),n:h.subarray(0,1),t:new nG(h.buffer,0,1),b:0}}else if(y==2)X=J1($,J,9-(_&1)),J=X[0],E[_]=X[1];else if(y==3){if(!Z.t)c$(0);E[_]=Z.t[_]}}var s=Z.t=E,f=s[0],c=s[1],o=s[2],e=$[H-1];if(!e)c$(0);var m=(H<<3)-8+L6(e)-o.b,u=m>>3,F0=0,B0=($[u]|$[u+1]<<8)>>(m&7)&(1<>3;var p=($[u]|$[u+1]<<8)>>(m&7)&(1<>3;var x0=($[u]|$[u+1]<<8)>>(m&7)&(1<>3;var h0=1<>>(m&7)&h0-1);u=(m-=dH[S])>>3;var v0=uj[S]+(($[u]|$[u+1]<<8|$[u+2]<<16)>>(m&7)&(1<>3;var N$=fj[d0]+(($[u]|$[u+1]<<8|$[u+2]<<16)>>(m&7)&(1<>3,B0=o.t[B0]+(($[u]|$[u+1]<<8)>>(m&7)&(1<>3,x0=f.t[x0]+(($[u]|$[u+1]<<8)>>(m&7)&(1<>3,p=c.t[p]+(($[u]|$[u+1]<<8)>>(m&7)&(1<3)Z.o[2]=Z.o[1],Z.o[1]=Z.o[0],Z.o[0]=I0-=3;else{var lZ=I0-(N$!=0);if(lZ){if(I0=lZ==3?Z.o[0]-1:Z.o[lZ],lZ>1)Z.o[2]=Z.o[1];Z.o[1]=Z.o[0],Z.o[0]=I0}else I0=Z.o[0]}for(var _=0;_v0)dZ=v0;for(var _=0;_{Ij=ArrayBuffer,h$=Uint8Array,nG=Uint16Array,_j=Int16Array,oG=Int32Array,gj=["invalid zstd data","window size too large (>2046MB)","invalid block type","FSE accuracy too high","match distance too far back","unexpected EOF"],hj=J1(new h$([81,16,99,140,49,198,24,99,12,33,196,24,99,102,102,134,70,146,4]),0,6)[1],bj=J1(new h$([33,20,196,24,99,140,33,132,16,66,8,33,132,16,66,8,33,68,68,68,68,68,68,68,68,36,9]),0,6)[1],mj=J1(new h$([32,132,16,66,102,70,68,68,68,68,36,73,2]),0,5)[1],lH=new h$(new oG([0,0,0,0,16843009,50528770,134678020,202050057,269422093]).buffer,0,36),fj=PN(lH,0),dH=new h$(new oG([0,0,0,0,0,0,0,0,16843009,50528770,117769220,185207048,252579084,16]).buffer,0,53),uj=PN(dH,3)});var UN={};P8(UN,{zstdDecompress:()=>A6,zstdCompress:()=>W1,isZstdCompressAvailable:()=>sH});function oH(){let $=import.meta.require??globalThis.require??null;if(!$)return null;try{return $(dj)}catch{return null}}function sH(){if(nH)return!0;return typeof oH()?.zstdCompressSync==="function"}async function W1($,Z){if(nH)return new Uint8Array(Bun.zstdCompressSync($,{level:Z}));let Q=oH(),X=Q?.zstdCompressSync;if(typeof X!=="function")throw Error("zstd compression is not available in this runtime. Requires Bun or Node.js >= 22.15 / Deno >= 2.6.9. (workerd has no native zstd encoder; fzstd is decompress-only.)");return new Uint8Array(X($,{params:{[Q.constants.ZSTD_c_compressionLevel]:Z}}))}async function A6($,Z){if(Z!=null){let X=nj($);if(X!==null&&X>Z)throw Error(`zstd decompressed size (${X}) would exceed cap (${Z})`)}let Q;if(nH)Q=new Uint8Array(Bun.zstdDecompressSync($));else{let J=oH()?.zstdDecompressSync;if(typeof J==="function")Q=new Uint8Array(J($));else{let W=FN($);Q=new Uint8Array(W.byteLength),Q.set(W)}}if(Z!=null&&Q.byteLength>Z)throw Error(`zstd decompressed size (${Q.byteLength}) exceeds cap (${Z})`);return Q}function nj($){if($.length<6)return null;if($[0]!==40||$[1]!==181||$[2]!==47||$[3]!==253)return null;let Z=$[4],Q=Z>>6&3,X=(Z>>5&1)===1,J=Z&3,W=Q===0?X?1:0:Q===1?2:Q===2?4:8;if(W===0)return null;let Y=X?0:1,G=J===0?0:J===1?1:J===2?2:4,H=5+Y+G;if($.lengthBigInt(Number.MAX_SAFE_INTEGER))return Number.MAX_SAFE_INTEGER;return Number(z)}var dj="node:zlib",nH;var Y1=O(()=>{NN();nH=typeof globalThis.Bun<"u"});function oj($){let Z=MN.get($);if(Z===void 0){let Q=[];for(let X of $.fields)if(Z1(X.type)&&X.type.bitWidth===64)Q.push(X.name);Z=Q,MN.set($,Z)}return Z}function iH($,Z){let Q=oj($);if(Q.length===0)return Z;let X=null;for(let J of Q){let W=Z[J];if(W===void 0)continue;if(Array.isArray(W)){let Y=null;for(let G=0;G0?W:typeof Z.name==="string"&&Z.name!=="Error"?Z.name:Z.constructor.name,G=Z.errorMessage,H=typeof G==="string"?G:Z.message;J.set(M6,`${Y}: ${H}`);let z=Z.errorKind??Z.constructor.errorKind;if(typeof z==="string"&&z.length>0)J.set(dG,z);let q={exception_type:Y,exception_message:H,traceback:Z.stack??""};if(typeof z==="string"&&z.length>0)q.error_kind=z;if(J.set(EJ,JSON.stringify(q)),J.set(wW,Q),X!==null)J.set(wJ,X);return GX($,J)}function LN($,Z,Q,X,J,W){let Y=new Map;if(Y.set(KQ,Z),Y.set(M6,Q),X)Y.set(EJ,JSON.stringify(X));if(J!=null)Y.set(wW,J);if(W!=null)Y.set(wJ,W);return GX($,Y)}function GX($,Z){return GQ($,Z)}var MN;var mX=O(()=>{qZ();MN=new WeakMap});function AN(){return typeof performance<"u"?performance.now():Date.now()}function sG($){let Z=new URL($);if(Z.protocol!=="https:")throw Error(`External location URL must use HTTPS, got "${Z.protocol}"`)}function O6($){try{let Z=new URL($);return Z.username="",Z.password="",Z.search="",Z.hash="",Z.toString()}catch{return""}}function aj($){let Z=$.maxFetchBytes??ij,Q=$.maxDecompressedBytes??Math.min(Number.MAX_SAFE_INTEGER,Z*16),X=$.maxRedirects??rj;if(!Number.isSafeInteger(Z)||Z<0)throw Error("maxFetchBytes must be a non-negative safe integer");if(!Number.isSafeInteger(Q)||Q<0)throw Error("maxDecompressedBytes must be a non-negative safe integer");if(!Number.isSafeInteger(X)||X<0)throw Error("maxRedirects must be a non-negative safe integer");return{maxFetchBytes:Z,maxDecompressedBytes:Q,maxRedirects:X}}async function tj($,Z,Q){let X=$.headers.get("Content-Length");if(X!=null){let z=Number(X);if(Number.isFinite(z)&&z>Z)throw Q.abort(),Error(`External location fetch exceeds max_fetch_bytes (${z} > ${Z})`)}if(!$.body)return new Uint8Array;let J=$.body.getReader(),W=[],Y=0;try{while(!0){let{done:z,value:q}=await J.read();if(z)break;if(Y+=q.byteLength,Y>Z)throw Q.abort(),Error(`External location fetch exceeded max_fetch_bytes (${Z} bytes)`);W.push(q)}}finally{try{await J.cancel()}catch{}J.releaseLock()}let G=new Uint8Array(Y),H=0;for(let z of W)G.set(z,H),H+=z.byteLength;return G}async function DN($){let Z=new ArrayBuffer($.byteLength);new Uint8Array(Z).set($);let Q=await crypto.subtle.digest("SHA-256",Z);return Array.from(new Uint8Array(Q)).map((X)=>X.toString(16).padStart(2,"0")).join("")}function F$($){if($.numRows!==0)return!1;let Z=$.metadata;if(!Z)return!1;return Z.has(cG)&&!Z.has(KQ)}function G1($,Z,Q){let X=new Map;if(X.set(cG,Z),Q)X.set(EH,Q);return GX($,X)}function ej($){return mZ($)}function $V($){return mZ($).byteLength}async function kJ($,Z,Q,X=!1){if(!Z?.storage)return $;if($.numRows===0)return $;let J=Z.externalizeThresholdBytes??sj;if(!X&&$V($)=Y)throw P.abort(),Error(`External location redirect limit exceeded (${Y})`);let g=q.headers.get("Location");if(!g)throw P.abort(),Error(`External location redirect had no Location header [url: ${O6(z)}]`);try{z=new URL(g,z).toString()}catch{throw P.abort(),Error(`External location redirect target is invalid [url: ${O6(z)}]`)}P.abort()}if(!q.ok)throw Error(`External location fetch failed: ${q.status} ${q.statusText} [url: ${O6(z)}]`);let U=await tj(q,J,P);if(q.headers.get("Content-Encoding")==="zstd")try{U=new Uint8Array(await A6(U,W))}catch(V){if(V instanceof Error&&/(?:decompressed size|\bcap\b)/i.test(V.message))throw Error(`External location decompressed body exceeds max_decompressed_bytes (${W})`);throw Error("External location zstd decompression failed")}if(U.byteLength>W)throw Error(`External location decompressed body exceeds max_decompressed_bytes (${U.byteLength} > ${W})`);let L=$.metadata?.get(EH);if(L){let V=await DN(U);if(V!==L)throw Error(`SHA-256 checksum mismatch for ${O6(z)}: expected ${L}, got ${V}`)}let D=null;for(let V of RH(U)){if(V.numRows===0&&O$(V,Q))continue;D??=V}if(D===null||D.numRows===0&&D.schema.fields.length===0)throw Error(`No data batch found in external IPC stream from ${O6(z)}`);let R=new Map(D.metadata??[]);return R.set(HN,(AN()-H).toFixed(1)),R.set(zN,X),e$(D,R)}var sj=1048576,ij=268435456,rj=5;var I8=O(()=>{qZ();pH();Y1();mX()});function ON($){if(!$)return[];let Z=[],Q=new Set;for(let X of $.split(",")){let J=X.trim().toLowerCase();if(!J)continue;let W=J.indexOf(";");if(W>=0)J=J.slice(0,W).trim();if(!ZV.includes(J))continue;if(Q.has(J))continue;Q.add(J),Z.push(J)}return Z}function jN($,Z,Q){let X=ON($),J=ON(Z),W=[...J,...X.filter((Y)=>!J.includes(Y))];for(let Y of W){if(Y==="identity")return{codec:null,usedCustom:!1};if(Q(Y))return{codec:Y,usedCustom:J.includes(Y)&&!X.includes(Y)}}return{codec:null,usedCustom:J.length>0}}function SW($){return $?"zstd, gzip":"gzip"}var CN=1,rH,ZV,C6="Content-Encoding",IJ="X-VGI-Content-Encoding",_J="X-VGI-Accept-Encoding",aH="VGI-Supported-Encodings";var BW=O(()=>{rH=["zstd","gzip"],ZV=[...rH,"identity"]});async function VN($,Z,Q){let X=Z.writable.getWriter(),J=Z.readable.getReader(),W=new Uint8Array($.byteLength);W.set($);let Y=(async()=>{await X.write(W),await X.close()})(),G=[],H=0;while(!0){let{value:P,done:U}=await J.read();if(U)break;let M=P;if(H+=M.byteLength,Q!=null&&H>Q)throw Error(`gzip decompressed size (${H}) exceeds cap (${Q})`);G.push(M)}await Y;let z=new Uint8Array(H),q=0;for(let P of G)z.set(P,q),q+=P.byteLength;return z}async function kW($,Z){return VN($,new DecompressionStream("gzip"),Z)}async function RN($,Z){return VN($,new CompressionStream("gzip"))}function K1($,Z,Q){return`${Q?.prefix??""}/${$}/${Z}${Q?.suffix??""}`}function j6($,Z){return`${Z?.prefix??""}/${$}`}function _8($,Z,Q){return`${$}/${Z}${Q?.suffix??""}`}async function tH($,Z,Q){if(!Z)return $;let X=Z.split(",").map((W)=>W.trim().toLowerCase()).filter((W)=>W.length>0).reverse(),J=$;for(let W of X)if(W==="zstd")J=await A6(J,Q);else if(W==="gzip")J=await kW(J,Q);return J}function QV($){let Z=[];if($.delete)Z.push(`${$.name}=`),Z.push("Max-Age=0");else{if(Z.push(`${$.name}=${$.value}`),$.maxAge!==void 0)Z.push(`Max-Age=${$.maxAge}`);if($.expires)Z.push(`Expires=${$.expires.toUTCString()}`)}if($.path)Z.push(`Path=${$.path}`);if($.domain)Z.push(`Domain=${$.domain}`);if($.secure)Z.push("Secure");if($.httpOnly)Z.push("HttpOnly");if($.sameSite)Z.push(`SameSite=${$.sameSite}`);if($.partitioned)Z.push("Partitioned");return Z.join("; ")}function aG($,Z){for(let Q of Z)$.append("Set-Cookie",QV(Q))}function f0($,Z){let Q=Z.map((X)=>U6(X,$));return N6($,Q)}function fZ($,Z=200,Q){let X=Q??new Headers;if(X.set("Content-Type",OZ),Z===500)return X.set(BJ,"true"),new Response($,{status:200,headers:X});return new Response($,{status:Z,headers:X})}async function tG($){let Z=kQ($);if(Z.schema.fields.length===0&&Z.numRows===0&&(Z.metadata?.size??0)===0)throw new w$("Empty IPC stream: no schema",400);return{schema:Z.schema,batch:Z}}var OZ="application/vnd.apache.arrow.stream",_Q="__upload_url__",IW=100,V6,TJ,fX="VGI-Session",iG="VGI-Session-Accept",_W="VGI-Session-Close",eH="VGI-Sticky-Enabled",$7="VGI-Sticky-Default-TTL",Z7="VGI-Sticky-Echo-Headers",TW="VGI-Echo-",rG="__session__",w$;var J8=O(()=>{qZ();Y1();V6=E0([O0("count",VJ(),!0)]),TJ=E0([O0("upload_url",Z$(),!1),O0("download_url",Z$(),!1),O0("expires_at",VH("UTC"),!1)]);w$=class w$ extends Error{statusCode;constructor($,Z){super($);this.statusCode=Z,this.name="HttpRpcError"}}});function XV($){if($.byteLength<4)return!1;if($[0]===40&&$[1]===181&&$[2]===47&&$[3]===253)return!0;return($[0]&240)===80&&$[1]===42&&$[2]===77&&$[3]===24}async function gJ($,Z,Q=JV){let X=EN($.headers),J=$.headers.get(IJ)?.trim().toLowerCase(),W=$.headers.get(C6)?.trim().toLowerCase(),Y=X.codec!==null,G=!J&&W!==void 0&&W!==null&&["gzip","deflate","br"].includes(W),H=Y?Q:Z,z=$.headers.get("Content-Length");if(!G&&z!=null&&/^[0-9]+$/.test(z)){let D=Number(z);if(Number.isSafeInteger(D)&&D>H)throw await $.body?.cancel("response limit exceeded"),new b("TransportError",`HTTP response exceeds ${Y?"representation safety":"accepted"} limit (${D} > ${H})`,"")}if(!$.body)return new Uint8Array;let q=$.body.getReader(),P=[],U=0;try{while(!0){let{value:D,done:R}=await q.read();if(R)break;if(U+=D.byteLength,U>H)throw await q.cancel("response limit exceeded"),new b("TransportError",`HTTP response exceeds ${Y?"representation safety":"accepted"} limit (${U} > ${H})`,"");P.push(D)}}finally{q.releaseLock()}let M=new Uint8Array(U),L=0;for(let D of P)M.set(D,L),L+=D.byteLength;return M}function EN($){let Z=$.get(IJ)?.trim().toLowerCase();if(Z&&Z!=="identity")return{codec:Z,custom:!0};let Q=$.get(C6)?.trim().toLowerCase();if(Q==="zstd")return{codec:"zstd",custom:!1};if(Q&&Q!=="identity"&&!["gzip","deflate","br"].includes(Q))return{codec:Q,custom:!1};return{codec:null,custom:!1}}async function gW($,Z,Q,X){let{codec:J,custom:W}=EN($);if(!J)return Z;if(J==="gzip")return new Uint8Array(await kW(Z,X));if(J==="zstd"){if(!W&&!XV(Z))return Z;if(!Q)throw new b("ProtocolError","Server sent a zstd-encoded response but this client has no zstd decoder. Install the optional zstd dependency, or configure the server not to negotiate zstd.","");let Y=new Uint8Array(await Q(Z,X));if(X!=null&&Y.byteLength>X)throw new b("TransportError",`Decoded HTTP response exceeds accepted limit (${Y.byteLength} > ${X})`,"");return Y}throw new b("ProtocolError",`Unsupported response encoding '${J}'${W?` (${IJ})`:` (${C6})`}.`,"")}var JV=268435456;var H1=O(()=>{HZ();BW()});function xW($,Z=!1){if(!$)throw Error("A protocol name may not be empty.");if(new TextEncoder().encode($).length>255)throw Error(`Protocol name exceeds 255 bytes: ${$.slice(0,64)}...`);if(!WV.test($))throw Error(`Protocol name '${$}' is not an identifier, optionally dot-qualified. Expected something like 'vgi.Identity.v1'.`);if(!Z&&$.startsWith("vgi_rpc."))throw Error(`Protocol name '${$}' claims the reserved 'vgi_rpc.' prefix, which is for protocols the framework defines.`)}function Q7($){return typeof $?.bindings==="function"}var WV,wN="vgi_rpc.",vW,T8;var z1=O(()=>{WV=/^[A-Za-z_][A-Za-z0-9_.]*$/;vW=class vW extends Error{errorKind="protocol_not_specified";constructor($,Z){super(Z??`Request carries no 'vgi_rpc.protocol' routing key. Every request must name the protocol it addresses. This server hosts: [${$.join(", ")}].`);this.name="ProtocolNotSpecifiedError"}static percentEncoded(){return new vW([],"The protocol path segment contains a percent sign. The protocol name charset never requires percent-encoding, so this is rejected rather than decoded.")}};T8=class T8 extends Error{errorKind="protocol_not_supported";constructor($){super($);this.name="ProtocolNotSupportedError"}static notHosted($,Z){return new T8(`This server does not host protocol '${$}'. Hosted: [${Z.join(", ")}].`)}}});function SN($){return $!=null&&typeof $.name==="string"&&$.type!=null&&typeof $.nullable==="boolean"}function YV($){return $!=null&&typeof $.typeId==="number"}function TQ($){let Z=$.fields;if(Array.isArray(Z)){let X=[];for(let J of Z)if(SN(J))X.push(J);else X.push(O0(J.name,J.type,J.nullable??!0,J.metadata));return E0(X)}let Q=[];for(let[X,J]of Object.entries($))if(SN(J))Q.push(J);else if(YV(J))Q.push(O0(X,J,!1));else throw TypeError(`Invalid schema value for "${X}": expected DataType or Field, got ${typeof J}`);return E0(Q)}function yW($){let Z=TQ($);if(Z.fields.length===0)return;let Q={};for(let X of Z.fields){let J;if(BH(X.type))J="str";else if(SH(X.type))J="bytes";else if(_H(X.type))J="bool";else if(wH(X.type))J="float";else if(Z1(X.type))J="int";if(!J)return;Q[X.name]=J}return Q}var BN,kN,IN,_N,TN,gN,xN,vN,yN,hN,bN,mN,fN;var X7=O(()=>{qZ();BN=Z$(),kN=X8(),IN=VJ(),_N=LH(),TN=MH(),gN=UH(),xN=AH(),vN=DH(),yN=OH(),hN=CH(),bN=tY(),mN=jH(),fN=jJ()});function hW($){let Z=new Uint8Array($);return crypto.getRandomValues(Z),Z}function uN($,Z){if($.length!==Z.length)return!1;let Q=0;for(let X=0;X<$.length;X++)Q|=$[X]^Z[X];return Q===0}async function GV($){let Z=await crypto.subtle.digest("SHA-256",$);return new Uint8Array(Z)}async function eG($){let Z=await GV($),Q="";for(let X=0;X{};function uX($,Z){for(let Q=0;Q<$.length;Q++){let X=$.charCodeAt(Q);if(X>=55296&&X<=56319){let J=$.charCodeAt(Q+1);if(!(J>=56320&&J<=57343))throw TypeError(`${Z} contains an unpaired surrogate`);Q++}else if(X>=56320&&X<=57343)throw TypeError(`${Z} contains an unpaired surrogate`)}}function W7($,Z="evidence",Q=0,X={values:0,sourceBytes:0}){if(Q>qV)throw TypeError(`${Z} exceeds maximum JSON depth`);if(X.values++,X.values>PV)throw TypeError(`${Z} exceeds maximum JSON value count`);if($===null||typeof $==="boolean")return $;if(typeof $==="string"){if(uX($,Z),$.length>$5)throw TypeError(`${Z} exceeds maximum JSON byte size`);if(X.sourceBytes+=new TextEncoder().encode($).length,X.sourceBytes>$5)throw TypeError(`${Z} exceeds maximum JSON byte size`);return $}if(typeof $==="number"){if(!Number.isFinite($))throw TypeError(`${Z} numbers must be finite`);return $}if(Array.isArray($))return Object.freeze($.map((J,W)=>W7(J,`${Z}[${W}]`,Q+1,X)));if(typeof $==="object"&&Object.getPrototypeOf($)===Object.prototype){let J={};for(let[W,Y]of Object.entries($)){if(uX(W,`${Z} key`),X.sourceBytes+=new TextEncoder().encode(W).length,X.sourceBytes>$5)throw TypeError(`${Z} exceeds maximum JSON byte size`);if(Y===void 0)throw TypeError(`${Z}.${W} is not JSON-compatible`);J[W]=W7(Y,`${Z}.${W}`,Q+1,X)}return Object.freeze(J)}throw TypeError(`${Z} is not JSON-compatible`)}function Y7($,Z){let Q=W7($??{},Z);if(new TextEncoder().encode(q1(Q)).length>$5)throw TypeError(`${Z} exceeds maximum JSON byte size`);return Q}function q1($){if($===null||typeof $!=="object")return JSON.stringify($);if(Array.isArray($))return`[${$.map(q1).join(",")}]`;let Z=$;return`{${Object.keys(Z).sort(cN).map((Q)=>`${JSON.stringify(Q)}:${q1(Z[Q])}`).join(",")}}`}function LV($){for(let Z of $){let Q=Z.codePointAt(0);if(Q<=31||Q===127)return!0}return!1}class P1{transport;immediatePeer;sourceEndpoint;assertedPeer;destinationAddress;authority;serviceName;metadata;deadline;budgetMs;#Z;#Q;constructor($,Z={}){if(!$)throw TypeError("peer transport must not be empty");uX($,"peer transport"),this.transport=$;for(let[W,Y]of Object.entries({immediatePeer:Z.immediatePeer,sourceEndpoint:Z.sourceEndpoint,assertedPeer:Z.assertedPeer,destinationAddress:Z.destinationAddress,authority:Z.authority,serviceName:Z.serviceName}))if(Y!==void 0)uX(Y,W);if(this.immediatePeer=Z.immediatePeer,this.sourceEndpoint=Z.sourceEndpoint,this.assertedPeer=Z.assertedPeer,this.destinationAddress=Z.destinationAddress,this.authority=Z.authority,this.serviceName=Z.serviceName,Z.deadline!==void 0&&(!Number.isFinite(Z.deadline)||Z.deadline<=0))throw TypeError("peer deadline must be a positive epoch millisecond value");if(this.deadline=Z.deadline,Z.budgetMs!==void 0&&(!Number.isFinite(Z.budgetMs)||Z.budgetMs<=0))throw TypeError("peer budgetMs must be positive");this.budgetMs=Z.budgetMs,this.#Z=performance.now(),this.metadata=Y7(Z.metadata,"peer metadata");let Q=new Map,X=Z.headers instanceof Map?Z.headers.entries():Object.entries(Z.headers??{}),J=0;for(let[W,Y]of X){if(Q.size>=FV)throw new A$("too many peer identity headers");if(uX(W,"peer-resolution header name"),!MV.test(W))throw TypeError("invalid peer-resolution header name");let G=W.toLowerCase();if(Q.has(G))throw new A$("case-varied duplicate peer identity header");if(!Array.isArray(Y))throw new A$(`peer identity header ${JSON.stringify(W)} did not preserve multiplicity`);let H=Object.freeze([...Y]);if(H.length>NV)throw new A$(`too many values for peer identity header: ${W}`);if(H.forEach((z)=>{uX(z,`peer-resolution header value: ${W}`)}),H.some((z)=>typeof z!=="string"||LV(z)))throw TypeError(`invalid peer-resolution header value: ${W}`);J+=new TextEncoder().encode(W).length;for(let z of H)J+=new TextEncoder().encode(z).length;if(J>UV)throw new A$("peer identity headers are too large");Q.set(G,H)}this.#Q=Q,Object.freeze(this)}header($){uX($,"peer-resolution header lookup");let Z=this.#Q.get($.toLowerCase())??[];if(Z.length>1)throw new A$(`duplicate peer identity header: ${$}`);return Z[0]}remainingBudgetMs(){return this.budgetMs===void 0?void 0:Math.max(0,this.budgetMs-(performance.now()-this.#Z))}}class pX{provider;evidenceSource;assurance;issuer;transport;subjectKind;subjectKey;subjectStability;subjectVerified;attributes;capabilities;capabilitiesVerified;sourceAddress;proxyAddress;constructor($){if(!$.provider||!$.evidenceSource||!$.issuer||!$.transport)throw TypeError("provider, evidenceSource, issuer, and transport are required");for(let[X,J]of Object.entries({provider:$.provider,evidenceSource:$.evidenceSource,issuer:$.issuer,transport:$.transport,subjectKey:$.subjectKey,sourceAddress:$.sourceAddress,proxyAddress:$.proxyAddress}))if(J!==void 0)uX(J,X);let Z=$.subjectStability??W8.NONE,Q=$.subjectKind??HX.UNKNOWN;if(!KV.has($.assurance))throw TypeError("invalid peer identity assurance");if(!HV.has(Q))throw TypeError("invalid peer subject kind");if(!zV.has(Z))throw TypeError("invalid peer subject stability");if($.subjectVerified&&!$.subjectKey)throw TypeError("verified peer identity requires subjectKey");if(!$.subjectKey&&Z!==W8.NONE)throw TypeError("subjectless peer identity must use none stability");this.provider=$.provider,this.evidenceSource=$.evidenceSource,this.assurance=$.assurance,this.issuer=$.issuer,this.transport=$.transport,this.subjectKind=Q,this.subjectKey=$.subjectKey,this.subjectStability=Z,this.subjectVerified=$.subjectVerified??!1,this.attributes=Y7($.attributes,"peer attributes"),this.capabilities=Y7($.capabilities,"peer capabilities"),this.capabilitiesVerified=$.capabilitiesVerified??!1,this.sourceAddress=$.sourceAddress,this.proxyAddress=$.proxyAddress,Object.freeze(this)}get canonicalPrincipal(){if(!this.subjectKey)throw TypeError("subjectless peer evidence has no canonical principal");return`peer/${J7(this.provider)}/${J7(this.issuer)}/${J7(this.subjectKey)}`}}function J7($){let Z="";for(let Q of new TextEncoder().encode($)){let X=String.fromCharCode(Q);Z+=/[A-Za-z0-9._~-]/.test(X)?X:`%${Q.toString(16).toUpperCase().padStart(2,"0")}`}return Z}class P${provider;status;identities;constructor($,Z,Q=[]){if(!$)throw TypeError("peer identity provider is required");if(uX($,"peer identity provider"),!pN.has(Z))throw TypeError("invalid peer identity status");if(Z===C0.AVAILABLE!==Q.length>0)throw TypeError("only an available result may carry identities");if(Q.some((X)=>X.provider!==$))throw TypeError("peer result provider mismatch");this.provider=$,this.status=Z,this.identities=Object.freeze([...Q]),Object.freeze(this)}static available($){return new P$($.provider,C0.AVAILABLE,[$])}}function AV($,Z){for(let Q=0;Q<$.length;Q++){let X=cN($[Q],Z[Q]);if(X!==0)return X}return 0}function cN($,Z){let Q=Array.from($,(J)=>J.codePointAt(0)),X=Array.from(Z,(J)=>J.codePointAt(0));for(let J=0;JX[J])return 1}return Q.length-X.length}function Z5($,Z){return Z}function lN($){return async(Z,Q)=>{return Z.requireAvailableProvider($),DV(Q,await Z.bindingDigest([$]))}}function bW($){return async(Z)=>{let Q=Z.requireUsableProvider($);return new zZ($,!0,Q.canonicalPrincipal,{issuer:Q.issuer,subject_kind:Q.subjectKind,assurance:Q.assurance,evidence_source:Q.evidenceSource,subject:Q.subjectKey,peer_evidence_binding:await Z.bindingDigest([$])})}}function dN(...$){if($.length===0)throw TypeError("at least one peer provider is required");return async(Z,Q)=>{for(let X of $){let J=Z.status(X);if(J===C0.INVALID||J===C0.UNTRUSTED_PROXY)throw new A$(`peer identity provider ${JSON.stringify(X)} rejected evidence`);if(Z.eligibleSubjects(X).length>1)throw new A$(`peer identity provider ${JSON.stringify(X)} produced ambiguous subjects`)}if(Q.authenticated)return Q;for(let X of $)if(Z.status(X)===C0.AVAILABLE&&Z.eligibleSubjects(X).length===1)return bW(X)(Z,Q);if($.some((X)=>Z.status(X)===C0.UNAVAILABLE||Z.status(X)===C0.PERMISSION_DENIED))throw new BZ("no usable authentication factor; a peer provider is unavailable");throw new A$("no configured provider produced a verified subject")}}function nN($,Z,Q=$[0]){if($.length===0||!Z)throw TypeError("all-of requires providers and an identity linker");if(!$.includes(Q))throw TypeError("principalProvider must be one of providers");return async(X,J)=>{if(!J.authenticated)throw new A$("all-of requires application authentication");let W=new Map;for(let G of $)W.set(G,X.requireUsableProvider(G));await Z(J,W);let Y=await bW(Q)(X,J);return new zZ(Y.domain,!0,Y.principal,{...Y.claims,application_domain:J.domain,application_principal:J.principal,peer_evidence_binding:await X.bindingDigest($,J)})}}function DV($,Z){return new zZ($.domain,$.authenticated,$.principal,{...$.claims,peer_evidence_binding:Z})}var C0,pN,KX,KV,HX,HV,W8,zV,$5=65536,qV=16,PV=4096,FV=128,NV=16,UV=65536,MV,cX,BZ,A$;var mW=O(()=>{q6();R6();C0={OFF:"off",NOT_APPLICABLE:"not_applicable",AVAILABLE:"available",UNAVAILABLE:"unavailable",PERMISSION_DENIED:"permission_denied",NO_MATCH:"no_match",INVALID:"invalid",UNTRUSTED_PROXY:"untrusted_proxy"},pN=new Set(Object.values(C0)),KX={CRYPTOGRAPHIC_PEER:"cryptographic_peer",LOCAL_DAEMON:"local_daemon",CONFIGURED_PROXY:"configured_proxy"},KV=new Set(Object.values(KX)),HX={USER:"user",TAGGED_NODE:"tagged_node",WORKLOAD:"workload",ENDPOINT:"endpoint",UNKNOWN:"unknown"},HV=new Set(Object.values(HX)),W8={STABLE:"stable",LOGIN:"login",NONE:"none"},zV=new Set(Object.values(W8)),MV=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;cX=class cX{static EMPTY=new cX;identities;#Z;constructor($=[]){let Z=new Map,Q=[];for(let X of $){if(!pN.has(X.status))throw TypeError(`invalid peer identity status: ${String(X.status)}`);if(Z.has(X.provider))throw TypeError(`duplicate peer identity provider: ${X.provider}`);Z.set(X.provider,X.status),Q.push(...X.identities)}this.#Z=Z,this.identities=Object.freeze(Q),Object.freeze(this)}status($){return this.#Z.get($)??C0.OFF}forProvider($){return Object.freeze(this.identities.filter((Z)=>Z.provider===$))}eligibleSubjects($){return Object.freeze(this.forProvider($).filter((Z)=>Z.subjectVerified&&!!Z.subjectKey&&Z.subjectStability===W8.STABLE))}uniqueVerifiedSubject($){let Z=this.eligibleSubjects($);if(Z.length!==1)throw new A$(`provider ${JSON.stringify($)} did not produce one verified stable subject`);return Z[0]}requireUsableProvider($){let Z=this.status($);if(Z===C0.UNAVAILABLE||Z===C0.PERMISSION_DENIED)throw new BZ(`peer identity provider ${JSON.stringify($)} is unavailable`);if(Z===C0.INVALID||Z===C0.UNTRUSTED_PROXY)throw new A$(`peer identity provider ${JSON.stringify($)} rejected evidence`,Z===C0.UNTRUSTED_PROXY?"proxy_required":"invalid_credential");return this.uniqueVerifiedSubject($)}requireAvailableProvider($){let Z=this.status($);if(Z===C0.UNAVAILABLE||Z===C0.PERMISSION_DENIED)throw new BZ(`peer identity provider ${JSON.stringify($)} is unavailable`);if(Z===C0.INVALID||Z===C0.UNTRUSTED_PROXY)throw new A$(`peer identity provider ${JSON.stringify($)} rejected evidence`,Z===C0.UNTRUSTED_PROXY?"proxy_required":"invalid_credential");let Q=this.forProvider($);if(Z!==C0.AVAILABLE||Q.length===0)throw new A$(`peer identity provider ${JSON.stringify($)} did not produce evidence`);return Q}async bindingDigest($,Z){let Q=[];for(let H of[...new Set($)].sort()){Q.push(H,this.status(H));let z=this.forProvider(H).map((q)=>[q.provider,q.issuer,q.subjectKey??"",q.assurance,q.evidenceSource,q.transport,q.subjectKind,q.subjectStability,String(q.subjectVerified),String(q.capabilitiesVerified),"","",q1(q.attributes),q1(q.capabilities)]).sort((q,P)=>AV(q,P));for(let q of z)Q.push(...q)}if(Z)Q.push("application_auth",Z.domain??"",Z.principal??"");let X=0,J=Q.map((H)=>{let z=new TextEncoder().encode(H);return X+=8+z.length,z}),W=new Uint8Array(X),Y=new DataView(W.buffer),G=0;for(let H of J)Y.setBigUint64(G,BigInt(H.length)),G+=8,W.set(H,G),G+=H.length;return eG(W)}};BZ=class BZ extends Error{retryAfter;constructor($="peer identity provider unavailable",Z=5){super($);this.name="PeerIdentityUnavailableError",this.retryAfter=Z}};A$=class A$ extends Error{vgiAuthReason;constructor($,Z="invalid_credential"){super($);this.name="PeerIdentityRejectedError",this.vgiAuthReason=Z}}});function oN(){return Error("setCookie/deleteCookie is only supported inside unary RPC methods served over HTTP")}function Q5($){return new sN($)}class xQ{_batches=[];_dataBatchIdx=null;_finished=!1;_producerMode;_outputSchema;_serverId;_requestId;_cookieSinkEnabled=!1;_responseCookies=[];_stickyContext=null;auth;peerEvidence;inputMetadata;cookies;kind;remainingResponseBytes;responseLimitBytes;preferredResponseBytes;remainingExternalizedResponseBytes;externalizationEnabled;constructor($,Z=!0,Q="",X=null,J,W,Y,G){this._outputSchema=$,this._producerMode=Z,this._serverId=Q,this._requestId=X,this.auth=J??zZ.anonymous(),this.peerEvidence=G?.peerEvidence??cX.EMPTY,this.inputMetadata=G?.inputMetadata,this.cookies=W??OV,this.kind=Y,this.remainingResponseBytes=G?.remainingResponseBytes,this.responseLimitBytes=G?.responseLimitBytes??G?.remainingResponseBytes,this.preferredResponseBytes=G?.preferredResponseBytes,this.remainingExternalizedResponseBytes=G?.remainingExternalizedResponseBytes,this.externalizationEnabled=G?.externalizationEnabled}enableCookieSink(){this._cookieSinkEnabled=!0}drainResponseCookies(){let $=this._responseCookies;return this._responseCookies=[],$}setCookie($,Z,Q){if(!this._cookieSinkEnabled)throw oN();this._responseCookies.push({name:$,value:Z,delete:!1,...Q??{}})}deleteCookie($,Z){if(!this._cookieSinkEnabled)throw oN();this._responseCookies.push({name:$,value:"",delete:!0,path:Z?.path,domain:Z?.domain})}attachStickyContext($){this._stickyContext=$}get session(){return this._stickyContext?.state??null}get sessionId(){return this._stickyContext?.sessionId??null}openSession($,Z){let Q=this._stickyContext;if(!Q)throw Q5("sticky sessions not available on this transport");if(!Q.acceptOpens)throw Q5("client did not opt in to sticky sessions "+"(missing VGI-Session-Accept: true header — open the call inside "+"an HttpConnection.with_session_token() block)");if(Q.state!==null)throw Q5("a sticky session is already active for this request");Q._open($,Z),Q.action="open"}closeSession(){let $=this._stickyContext;if(!$)throw Q5("sticky sessions not available on this transport");$._close(),$.action="close"}get outputSchema(){return this._outputSchema}get finished(){return this._finished}get batches(){return this._batches}get dataBatchIdx(){return this._dataBatchIdx}emit($,Z){let Q;if(fH($))Q=$;else{let X=iH(this._outputSchema,$),J={};for(let W of this._outputSchema.fields){let Y=X[W.name];J[W.name]=Array.isArray(Y)?Y:[Y]}Q=YQ(this._outputSchema,J)}if(this._dataBatchIdx!==null)throw new b("ProtocolError","Only one data batch may be emitted per call","");this._dataBatchIdx=this._batches.length,this._batches.push({batch:Q,metadata:Z})}emitRow($){let Z={};for(let[Q,X]of Object.entries($))Z[Q]=[X];this.emit(Z)}finish(){if(!this._producerMode)throw Error("finish() is not allowed on exchange streams; exchange streams must emit exactly one data batch per call");this._finished=!0}clientLog($,Z,Q){let X=LN(this._outputSchema,$,Z,Q,this._serverId,this._requestId);this._batches.push({batch:X})}}var S$,gQ,OV,sN;var lX=O(()=>{qZ();q6();HZ();mW();mX();(function($){$.UNARY="unary",$.STREAM="stream"})(S$||(S$={}));(function($){$.PIPE="pipe",$.HTTP="http",$.UNIX="unix",$.TCP="tcp"})(gQ||(gQ={}));OV=new Map;sN=class sN extends Error{constructor($){super($);this.name="RuntimeError"}}});class E6{name;protocolVersion;protocolVersionParts;_methods=new Map;constructor($,Z){this.name=$;let Q=Z?.protocolVersion;if(Q===void 0||Q==="")this.protocolVersion="",this.protocolVersionParts=null;else this.protocolVersion=Q,this.protocolVersionParts=VW(Q)}unary($,Z){let Q=TQ(Z.params);return this._methods.set($,{name:$,type:S$.UNARY,paramsSchema:Q,resultSchema:TQ(Z.result),handler:Z.handler,doc:Z.doc,defaults:Z.defaults,paramTypes:Z.paramTypes??yW(Q)}),this}producer($,Z){let Q=TQ(Z.params);return this._methods.set($,{name:$,type:S$.STREAM,paramsSchema:Q,resultSchema:G7,outputSchema:TQ(Z.outputSchema),inputSchema:G7,producerInit:Z.init,producerFn:Z.produce,onCancel:Z.onCancel,headerSchema:Z.headerSchema?TQ(Z.headerSchema):void 0,headerInit:Z.headerInit,doc:Z.doc,defaults:Z.defaults,paramTypes:Z.paramTypes??yW(Q)}),this}exchange($,Z){let Q=TQ(Z.params);return this._methods.set($,{name:$,type:S$.STREAM,paramsSchema:Q,resultSchema:G7,inputSchema:TQ(Z.inputSchema),outputSchema:TQ(Z.outputSchema),exchangeInit:Z.init,exchangeFn:Z.exchange,onCancel:Z.onCancel,headerSchema:Z.headerSchema?TQ(Z.headerSchema):void 0,headerInit:Z.headerInit,doc:Z.doc,defaults:Z.defaults,paramTypes:Z.paramTypes??yW(Q)}),this}getMethods(){return new Map(this._methods)}getMethod($){return this._methods.get($)}methodNames(){return[...this._methods.keys()].sort()}}var G7;var X5=O(()=>{qZ();HZ();X7();lX();G7=E0([])});function J5($){if(typeof $==="number"&&$>=0&&$`;case g0.FixedSizeList:return`fixed_size_list(${Number(Z.listSize)})<${F1(H7(Z),"item")}>`;case g0.Struct:return`struct<${z7(Z).map(iN).join(",")}>`;case g0.Map:{let Q=z7(H7(Z).type);if(Q.length!==2)throw new fW($);let X=`map<${F1(Q[0],"key")},${F1(Q[1],"value")}>`;return Z.keysSorted?`${X},keys_sorted`:X}case g0.Dictionary:{let{indices:Q,dictionary:X}=Z;if(!Q||!X)throw new fW($);let J=`dictionary`;return Z.isOrdered?`${J},ordered`:J}case g0.Union:{let Q=Z.typeIds??[],X=z7(Z).map((W,Y)=>`${Q[Y]??Y}=${iN(W)}`);return`${Number(Z.mode??0)===0?"sparse_union":"dense_union"}<${X.join(",")}>`}default:throw new fW($)}}function H7($){let Z=$.children??[];if(Z.length!==1)throw new fW($);return Z[0]}function z7($){return $.children??[]}function Y5($){if(!$)return[];return $.map((Z)=>({name:Z.name,nullable:Z.nullable,type:W5(Z.type)}))}var fW,K7;var rN=O(()=>{uH();fW=class fW extends Error{constructor($){super(`Arrow type ${String($)} has no canonical token. Add one to src/type-tokens.ts and to every other port at the same time: a one-sided addition changes only this port's protocol hash.`);this.name="UnsupportedArrowTypeError"}};K7=["s","ms","us","ns"]});function jV($,Z){let X=[...Z].sort((J,W)=>J.nameW.name?1:0).map((J)=>{let W={name:J.name,type:J.methodType,has_return:J.hasReturn,has_header:J.hasHeader,params:Y5(J.paramsFields)};if(J.hasReturn&&J.resultFields)W.result=Y5(J.resultFields);if(J.hasHeader&&J.headerFields)W.header=Y5(J.headerFields);return W});return{protocol:$,methods:X}}function q7($){if($===null)return"null";if(typeof $==="boolean")return $?"true":"false";if(typeof $==="string")return JSON.stringify($);if(typeof $==="number"||typeof $==="bigint")throw TypeError(`The protocol-hash preimage carries no numbers, but found ${String($)}. Fold it into a type token (e.g. 'decimal128(38,9)') instead.`);if(Array.isArray($))return`[${$.map(q7).join(",")}]`;if(typeof $==="object"){let Z=$;return`{${Object.keys(Z).sort().map((X)=>`${JSON.stringify(X)}:${q7(Z[X])}`).join(",")}}`}throw TypeError(`Unsupported value in the protocol-hash preimage: ${String($)}`)}function VV($,Z){return q7(jV($,Z))}async function aN($,Z){let Q=new TextEncoder().encode(CV+VV($,Z)),X=await crypto.subtle.digest("SHA-256",Q);return[...new Uint8Array(X)].map((J)=>J.toString(16).padStart(2,"0")).join("")}var CV="vgi_rpc.protocol_hash.v1|";var tN=O(()=>{rN()});function U1(){return`'${pW}' was retired. Introspection is now the '${uZ}' protocol: call '${N1}' for what this server hosts, then '${w6}' for one protocol's methods.`}function eN($,Z){let Q={};for(let X of Z.fields)Q[X.name]=[$[X.name]];return mZ(YQ(Z,Q))}function G5($,Z){return Error(`Could not decode a '${uZ}' ${$}: ${Z}. The reply came from a server this client cannot read; check that it hosts a compatible major version of the reflection protocol.`)}function g8($,Z,Q){let X=$[Z];if(X===void 0||X===null)throw G5(Q,`it carries no '${Z}', and that field has no default`);return X}function uW($,Z,Q){let X=$[Z];return X===void 0||X===null?Q:X}function P7($,Z){let Q=$[Z];if(Q===void 0||Q===null)return new Uint8Array(0);return Q}function XU($,Z,Q){let X=$.getChild(Z);if(X===null)throw G5(Q,`the payload has no '${Z}' column`);let J=X.get(0);if(J===void 0||J===null)throw G5(Q,`its '${Z}' column is null`);return[...J]}function JU($,Z){let Q=kQ($);if(Q.numRows<1)throw G5(Z,"the payload carries no rows");return Q}function WU($){return{protocol:g8($,"protocol","protocol summary"),protocol_version:g8($,"protocol_version","protocol summary"),protocol_hash:g8($,"protocol_hash","protocol summary"),deprecated:uW($,"deprecated",!1),deprecation_message:uW($,"deprecation_message",""),features:[...uW($,"features",[])].map(String)}}function SV($){return{name:g8($,"name","method description"),method_type:g8($,"method_type","method description"),has_return:g8($,"has_return","method description"),has_header:g8($,"has_header","method description"),stream_kind:g8($,"stream_kind","method description"),params_schema_ipc:P7($,"params_schema_ipc"),result_schema_ipc:P7($,"result_schema_ipc"),header_schema_ipc:P7($,"header_schema_ipc"),idempotency:uW($,"idempotency","unknown"),deprecated:uW($,"deprecated",!1),deprecation_message:uW($,"deprecation_message","")}}function K5($){let Z=JU($,"protocol listing"),Q={server_id:Z.getChild("server_id")?.get(0),server_version:Z.getChild("server_version")?.get(0),request_version:Z.getChild("request_version")?.get(0)};return{server_id:g8(Q,"server_id","protocol listing"),server_version:g8(Q,"server_version","protocol listing"),request_version:g8(Q,"request_version","protocol listing"),protocols:XU(Z,"protocols","protocol listing").map(WU)}}function H5($){let Z=JU($,"service description"),Q={};for(let X of Z.schema.fields)if(X.name!=="methods")Q[X.name]=Z.getChild(X.name)?.get(0);return{...WU(Q),methods:XU(Z,"methods","service description").map(SV)}}function F7($){if(!$)return new Uint8Array(0);return eY($)}function BV($){if($.type===S$.UNARY)return"";if($.exchangeFn)return"exchange";if($.producerFn)return"producer";return"unknown"}function N7($){return $.type===S$.UNARY&&($.resultSchema?.fields.length??0)>0}function kV($){return{name:$.name,method_type:$.type===S$.UNARY?"unary":"stream",has_return:N7($),has_header:$.headerSchema!==void 0,stream_kind:BV($),params_schema_ipc:F7($.paramsSchema),result_schema_ipc:N7($)?F7($.resultSchema):new Uint8Array(0),header_schema_ipc:F7($.headerSchema),idempotency:"unknown",deprecated:!1,deprecation_message:""}}async function IV($,Z){let Q=[];for(let X of Z.values()){let J={name:X.name,methodType:X.type===S$.UNARY?"unary":"stream",hasReturn:N7(X),hasHeader:X.headerSchema!==void 0,paramsFields:X.paramsSchema?.fields??[]};if(J.hasReturn)J.resultFields=X.resultSchema?.fields??[];if(J.hasHeader)J.headerFields=X.headerSchema?.fields??[];Q.push(J)}return aN($,Q)}function cW($){let Z=$U.get($.protocol);if(!Z)Z=new Map,$U.set($.protocol,Z);let Q=Z.get($.name);if(!Q)Q=IV($.name,$.protocol.getMethods()),Z.set($.name,Q);return Q}function z5($){let Z=new E6(uZ);return Z.unary("list_protocols",{params:{},result:{result:X8()},doc:"Return every protocol this server hosts, with versions and hashes.",handler:async()=>{let Q=$.listBindings(),X=[...Q.keys()].sort(),J=[];for(let Y of X){let G=Q.get(Y);J.push({protocol:G.name,protocol_version:G.protocol.protocolVersion??"",protocol_hash:await $.hashFor(Y),deprecated:!1,deprecation_message:"",features:[]})}let W={server_id:$.serverId(),server_version:$.serverVersion(),request_version:IQ,protocols:J};return{result:eN(W,EV)}}}),Z.unary("describe",{params:{protocol:Z$()},result:{result:X8()},doc:"Return one protocol's full description.",handler:async(Q)=>{let X=String(Q.protocol??""),J=$.listBindings(),W=J.get(X);if(!W)throw T8.notHosted(X,[...J.keys()].sort());let Y=[...W.protocol.getMethods().values()].sort((H,z)=>H.namez.name?1:0).map(kV),G={protocol:W.name,protocol_version:W.protocol.protocolVersion??"",protocol_hash:await $.hashFor(X),deprecated:!1,deprecation_message:"",features:[],methods:Y};return{result:eN(G,wV)}}}),Z}var uZ="vgi_rpc.Reflection.v1",N1="list_protocols",w6="describe",ZU,pW="__describe__",QU,RV,EV,wV,$U;var M1=O(()=>{qZ();z1();X5();tN();lX();ZU=E0([O0("protocol",Z$(),!1)]);QU=[O0("protocol",Z$(),!1),O0("protocol_version",Z$(),!1),O0("protocol_hash",Z$(),!1),O0("deprecated",jJ(),!1),O0("deprecation_message",Z$(),!1),O0("features",P6(O0("item",Z$(),!0)),!1)],RV=[O0("name",Z$(),!1),O0("method_type",Z$(),!1),O0("has_return",jJ(),!1),O0("has_header",jJ(),!1),O0("stream_kind",Z$(),!1),O0("params_schema_ipc",X8(),!1),O0("result_schema_ipc",X8(),!1),O0("header_schema_ipc",X8(),!1),O0("idempotency",Z$(),!1),O0("deprecated",jJ(),!1),O0("deprecation_message",Z$(),!1)],EV=E0([O0("server_id",Z$(),!1),O0("server_version",Z$(),!1),O0("request_version",Z$(),!1),O0("protocols",P6(O0("item",pG(QU),!0)),!1)]),wV=E0([...QU,O0("methods",P6(O0("item",pG(RV),!0)),!1)]);$U=new WeakMap});function TV($){let Z=$;return typeof Z?.read==="function"&&typeof Z?.pipe==="function"}function gV($){return new Proxy($,{get(Z,Q){if(Q==="read")return(J)=>Z.read(typeof J==="number"?Math.min(J,_V):J);let X=Reflect.get(Z,Q,Z);return typeof X==="function"?X.bind(Z):X}})}class xJ{reader;initialized=!1;streamEnded=!1;constructor($){this.reader=$}static async create($){let Z=TV($)?gV($):$,Q=await z$.from(Z);if(await Q.open({autoDestroy:!1}),Q.closed)throw Error("Input stream closed before first IPC message");return new xJ(Q)}async readStream(){if(this.initialized){if(await this.reader.reset().open(),this.reader.closed)return null}this.initialized=!0;let $=this.reader.schema;if(!$)return null;let Z=[];while(!0){let Q=await this.reader.next();if(Q.done)break;if(Q.value.constructor.name==="_InternalEmptyPlaceholderRecordBatch")break;Z.push(Q.value)}return{schema:$,batches:Z}}async openNextStream(){if(this.initialized){if(await this.reader.reset().open(),this.reader.closed)return null}return this.initialized=!0,this.streamEnded=!1,this.reader.schema??null}async readNextBatch(){if(this.streamEnded)return null;let $=await this.reader.next();if($.done)return this.streamEnded=!0,null;if($.value.constructor.name==="_InternalEmptyPlaceholderRecordBatch")return this.streamEnded=!0,null;return $.value}async cancel(){await this.reader.cancel()}}var _V=67108864;var q5=O(()=>{JQ()});function P5($){if(typeof $==="string")return new T$;if(typeof $==="boolean")return new V$;if(typeof $==="bigint")return new AZ;if(typeof $==="number")return new $Q;if($ instanceof Uint8Array)return new xZ;return new T$}function U7($,Z){if(Z==null)return Z;if(k.isInt($)&&$.bitWidth===64){if(typeof Z==="number")return BigInt(Z);return Z}if(k.isMap($)){if(Z instanceof Map){let X=$.children[0].type.children[1].type,J=new Map;for(let[W,Y]of Z)J.set(W,U7(X,Y));return J}return Z}if(k.isList($)){if(Array.isArray(Z)){let Q=$.children[0].type;return Z.map((X)=>U7(Q,X))}return Z}return Z}function vQ($,Z,Q,X){let J=new Map;if(J.set(yX,Q),X?.protocol)J.set(RJ,X.protocol);if(J.set(hX,IQ),X?.protocolVersion)J.set(SJ,X.protocolVersion);if($.fields.length===0){let G=GQ($,J);return f0($,[G])}let W={};for(let G of $.fields){let H=Z[G.name];W[G.name]=H===void 0?null:U7(G.type,H)}let Y=vX($,W,J);return f0($,[Y])}async function zX($){let Z=await z$.from($);await Z.open();let Q=Z.schema;if(!Q)throw new b("ProtocolError","Empty IPC stream: no schema","");let X=Z.readAll();return{schema:Q,batches:X}}function HQ($){let Z=[];for(let Q=0;Q<$.numRows;Q++){let X={};for(let J=0;J<$.schema.fields.length;J++){let W=$.schema.fields[J],Y=$.getChildAt(J)?.get(Q);if(typeof Y==="bigint"){if(Y>=BigInt(Number.MIN_SAFE_INTEGER)&&Y<=BigInt(Number.MAX_SAFE_INTEGER))Y=Number(Y)}X[W.name]=Y}Z.push(X)}return Z}async function L1($){let Z=new ReadableStream({start(Q){Q.enqueue($),Q.close()}});return xJ.create(Z)}var S6=O(()=>{JQ();WX();HZ();J8();q5();pH()});function M7($){if($.length===0)return new Q0([]);return $1($)}function xV($){let Z=$.method_type==="stream"?"stream":"unary",Q={name:$.name,type:Z,hasReturn:$.has_return,paramsSchema:M7($.params_schema_ipc),resultSchema:M7($.result_schema_ipc)};if(Z==="stream")Q.streamKind=$.stream_kind;if($.has_header)Q.headerSchema=M7($.header_schema_ipc);return Q}function A1($,Z){return{protocolName:$.protocol,protocolVersion:$.protocol_version,protocolHash:$.protocol_hash,hostedProtocols:Z?Z.protocols.map((Q)=>Q.protocol):[],methods:$.methods.map(xV),serverId:Z?.server_id,requestVersion:Z?.request_version}}function D1($){let Z=$.protocols.find((Q)=>!Q.protocol.startsWith(wN));if(!Z)throw new b("ProtocolError",`Server ${$.server_id} hosts no application protocol: it offers only [${$.protocols.map((Q)=>Q.protocol).join(", ")}]. Name a protocol explicitly to describe one of those.`,"");return Z.protocol}async function L7($,Z,Q){let X=null;for(let Y of $){if(Y.numRows===0){if(F$(Y)){X=await E$(Y,Q,Z);continue}O$(Y,Z);continue}X=Y}if(!X)throw new b("ProtocolError",`Empty '${uZ}' response`,"");let W=(X.getChild("result")??X.getChildAt(0))?.get(0);if(!(W instanceof Uint8Array))throw new b("ProtocolError",`A '${uZ}' reply carried no 'result' column of bytes`,"");return W}function A7($,Z){if($===w6)return vQ(ZU,{protocol:Z},w6,{protocol:uZ});return vQ(E0([]),{},$,{protocol:uZ})}async function O1($,Z){let Q=$.replace(/\/+$/,""),X=Z?.prefix??"",J={"Content-Type":OZ};if(Z?.authorization)J.Authorization=Z.authorization;let W=Z?.compressionLevel,Y=Z?.compressFn,G=Z?.decompressFn;if(W!=null&&Y)J["Content-Encoding"]="zstd";if(W!=null&&G)J["Accept-Encoding"]="zstd";J[_J]=SW(G!=null);let H=Z?.acceptedMaxResponseBytes??JX;WQ(H,"acceptedMaxResponseBytes");let z=H;if(!Z?.responseBudgetVerified){let L=await k8(Q,X,Z?.authorization,H,Z?.fetch??globalThis.fetch);if(!L.acceptMaxResponseBytesSupport)throw new b("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true before RPC dispatch","");z=t$(H,L.maxResponseBytes??void 0)??H}J[bZ]=String(H);let q=Z?.fetch??globalThis.fetch;async function P(L,D){let R=A7(L,D),V=W!=null&&Y?await Y(R,W):R,g=await q(Q+K1(uZ,L,{prefix:X}),{method:"POST",headers:J,body:V});if(g.status===401)throw new b("AuthenticationError","Authentication required","");let w=CJ(g.headers);z=t$(z,w.maxResponseBytes??void 0)??z;let T=await gJ(g,z),E=new Uint8Array(await gW(g.headers,T,G,z)),{batches:_}=await zX(E);return L7(_,void 0,Z?.externalLocation)}let U,M=Z?.protocol;if(!M)U=K5(await P(N1)),M=D1(U);return A1(H5(await P(w6,M)),U)}var F5=O(()=>{JQ();WX();z1();HZ();I8();BW();J8();OJ();M1();RW();H1();S6()});function N5($){if(aY.name==="arrow-js")return"active";let Z=$;return Z!=null&&typeof Z.data==="object"&&Z.data!==null&&!Array.isArray(Z.children)?"arrow-js":"active"}function GU($,Z){if(N5($)===Z)return $;return Z==="active"?kQ(mZ($)):kQ(mZ($))}function U5($,Z){let Q=$;return N5(Q)==="arrow-js"?e$(Q,Z):e$(Q,Z)}function vJ($,Z){let Q=Z;if(Q.length>0&&Q.every((X)=>N5(X)==="arrow-js"))return N6(Q[0].schema,Q);return f0($,Q.map((X)=>GU(X,"active")))}function KU($){let Z=null,Q=null,X=(J,W)=>{return Z=J,Q=J==="arrow-js"?F6(W):F6(W),Q.start()};return{writeBatch(J){let W=J,Y=new Uint8Array(0);if(Q===null){let H=N5(W);Y=X(H,H==="arrow-js"?W.schema:$)}let G=Q.writeBatch(GU(W,Z));return YU(Y,G)},finish(){let J=Q===null?X("active",$):new Uint8Array(0);return YU(J,Q.finish())}}}function YU($,Z){if($.length===0)return Z;if(Z.length===0)return $;let Q=new Uint8Array($.length+Z.length);return Q.set($,0),Q.set(Z,$.length),Q}var C1=O(()=>{WX();WX();J8()});function zQ($){return{batch:$,metadata:new Map($.metadata??[])}}function dX($,Z){let Q=new Map($.metadata);if(Z)for(let[X,J]of Z)Q.set(X,J);return U5($.batch,Q)}var M5=O(()=>{C1()});function vV($,Z){return Z===null?$:`${$.length}:${$}${Z}`}function D7($){let Z=$.indexOf(":");if(Z<0)return{cursor:$,callToken:null};let Q=Number($.slice(0,Z));if(!Number.isInteger(Q)||Q<0)return{cursor:$,callToken:null};let X=$.slice(Z+1);if(Q>X.length)return{cursor:$,callToken:null};let J=X.slice(Q);return{cursor:X.slice(0,Q),callToken:J===""?null:J}}var B6;var O7=O(()=>{WX();HZ();I8();BW();J8();OJ();RW();H1();S6();C1();M5();B6=class B6{_baseUrl;_prefix;_method;_stateToken;_callStateToken;_outputSchema;_inputSchema;_onLog;_pendingBatches;_finished;_header;_rawHeader;_compressionLevel;_compressFn;_decompressFn;_authorization;_externalConfig;_postFn;_acceptedMaxResponseBytes;_responseBudgetSupport=null;constructor($){if(this._baseUrl=$.baseUrl,this._prefix=$.prefix,this._method=$.method,this._stateToken=$.stateToken,this._callStateToken=$.callStateToken??null,this._outputSchema=$.outputSchema,this._inputSchema=$.inputSchema,this._onLog=$.onLog,this._pendingBatches=$.pendingBatches,this._finished=$.finished,this._header=$.header,this._rawHeader=$.rawHeader??null,this._compressionLevel=$.compressionLevel,this._compressFn=$.compressFn,this._decompressFn=$.decompressFn,this._authorization=$.authorization,this._externalConfig=$.externalConfig,this._postFn=$.postFn,this._acceptedMaxResponseBytes=$.acceptedMaxResponseBytes??JX,WQ(this._acceptedMaxResponseBytes,"acceptedMaxResponseBytes"),$.pendingBatches.filter((Q)=>Q.numRows>0||F$(Q)).length>1)throw new b("ProtocolError","A stream init returned more than one data batch","")}async _post($,Z){if(this._postFn)return this._postFn($,Z);if(!this._responseBudgetSupport)this._responseBudgetSupport=k8(this._baseUrl,this._prefix,this._authorization,this._acceptedMaxResponseBytes).then((J)=>{if(!J.acceptMaxResponseBytesSupport)throw new b("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true before RPC dispatch","");this._acceptedMaxResponseBytes=t$(this._acceptedMaxResponseBytes,J.maxResponseBytes??void 0)??this._acceptedMaxResponseBytes});await this._responseBudgetSupport;let Q=await fetch($,{method:"POST",headers:this._buildHeaders(),body:await this._prepareBody(Z)}),X=CJ(Q.headers);return this._acceptedMaxResponseBytes=t$(this._acceptedMaxResponseBytes,X.maxResponseBytes??void 0)??this._acceptedMaxResponseBytes,Q}get header(){return this._header}get rawHeader(){return this._rawHeader}_tokenMetadata($,Z){let Q=new Map;if(Q.set(y$,$),Z?.cancel)Q.set(bX,"1");if(this._callStateToken!==null)Q.set(YX,this._callStateToken);return Q}_resumeToken(){return this._stateToken===null?null:vV(this._stateToken,this._callStateToken)}_buildHeaders(){let $={"Content-Type":OZ};if(this._compressionLevel!=null&&this._compressFn)$["Content-Encoding"]="zstd";if(this._compressionLevel!=null&&this._decompressFn)$["Accept-Encoding"]="zstd";if($[_J]=SW(this._decompressFn!=null),$[bZ]=String(this._acceptedMaxResponseBytes),this._authorization)$.Authorization=this._authorization;return $}async _prepareBody($){if(this._compressionLevel!=null&&this._compressFn)return await this._compressFn($,this._compressionLevel);return $}async _readResponse($){let Z=await gJ($,this._acceptedMaxResponseBytes),Q=new Uint8Array(await gW($.headers,Z,this._decompressFn,this._acceptedMaxResponseBytes));if(Q.byteLength>this._acceptedMaxResponseBytes)throw new b("TransportError",`Decoded HTTP response exceeds accepted limit (${Q.byteLength} > ${this._acceptedMaxResponseBytes})`,"");return Q}async exchange($){if(this._stateToken===null)throw new b("ProtocolError","Stream has finished — no state token available","");if(!Array.isArray($)){let J=new Map($.metadata??[]);for(let[Y,G]of this._tokenMetadata(this._stateToken))J.set(Y,G);let W=U5($,J);return this._rowsOfExchange(await this._doExchange(W.schema,[W]))}if($.length===0){let J=this._inputSchema??this._outputSchema,W=GQ(J,this._tokenMetadata(this._stateToken));return this._rowsOfExchange(await this._doExchange(J,[W]))}let Z=this._inputSchema;if(!Z){let W=Object.keys($[0]).map((Y)=>{let G;for(let q of $)if(q[Y]!=null){G=q[Y];break}let H=P5(G),z=$.some((q)=>q[Y]==null);return O0(Y,H,z)});Z=E0(W)}let Q={};for(let J of Z.fields)Q[J.name]=$.map((W)=>W[J.name]);let X=e$(YQ(Z,Q),this._tokenMetadata(this._stateToken));return this._rowsOfExchange(await this._doExchange(Z,[X]))}async tick($){let Z=await this.tickRaw($);return Z===null?[]:HQ(Z.batch)}async tickRaw($){let Z=await this.nextWithTokenRaw($);return Z===null?null:Z.item}async exchangeRaw($){if(this._stateToken===null)throw new b("ProtocolError","Stream has finished — no state token available","");let Z=dX($,this._tokenMetadata(this._stateToken)),Q=await this._doExchange(Z.schema,[Z]);return Q===null?null:zQ(Q)}async cancel(){if(this._finished||this._stateToken===null){this._finished=!0,this._stateToken=null;return}let $=this._stateToken;this._finished=!0,this._stateToken=null;let Z=E0([]),Q=GQ(Z,this._tokenMetadata($,{cancel:!0}));try{let X=await this._post(this._baseUrl+_8(this._prefix,this._method,{suffix:"/exchange"}),f0(Z,[Q]));await this._readResponse(X)}catch{}}async _doExchange($,Z){let Q=vJ($,Z),X=await this._post(this._baseUrl+_8(this._prefix,this._method,{suffix:"/exchange"}),Q);if(X.status===401)throw new b("AuthenticationError","Authentication required","");let J=await this._readResponse(X),{batches:W}=await zX(J),Y=null;for(let G of W){let H=F$(G);if(G.numRows===0&&!H&&O$(G,this._onLog))continue;let z=H?await E$(G,this._externalConfig,this._onLog):G,q=z.metadata?.get(y$)??G.metadata?.get(y$);if(Y!==null){if(q&&G.numRows===0&&!H){this._stateToken=q;continue}throw new b("ProtocolError","An exchange turn returned more than one data batch","")}if(q)this._stateToken=q;Y=z}return Y}_rowsOfExchange($){return $===null?[]:HQ($)}async*[Symbol.asyncIterator](){for(let $ of this._pendingBatches){if($.numRows===0)if(F$($))$=await E$($,this._externalConfig,this._onLog);else{O$($,this._onLog);continue}yield HQ($)}if(this._pendingBatches=[],this._finished)return;if(this._stateToken===null)return;while(!0){let $=this._stateToken;if($===null)return;let Z=await this._sendContinuation($),{batches:Q}=await zX(Z),X=!1,J=null;for(let W of Q){if(W.numRows===0){let Y=W.metadata?.get(y$);if(Y){this._stateToken=Y,X=!0;continue}if(F$(W))W=await E$(W,this._externalConfig,this._onLog);else{O$(W,this._onLog);continue}}if(J!==null)throw new b("ProtocolError","A producer turn returned more than one data batch","");J=HQ(W)}if(J!==null)yield J;if(!X)break}}async nextWithToken(){let $=await this.nextWithTokenRaw();return $===null?null:{rows:HQ($.item.batch),token:$.token}}async nextWithTokenRaw($){if($!==void 0&&this._pendingBatches.length>0)throw new b("ProtocolError","Consume the producer's init batch before sending an explicit tick","");while(this._pendingBatches.length>0){let Y=this._pendingBatches.shift();if(Y.numRows===0)if(F$(Y))Y=await E$(Y,this._externalConfig,this._onLog);else{O$(Y,this._onLog);continue}if(this._pendingBatches.some((G)=>G.numRows>0||F$(G)))throw new b("ProtocolError","A producer turn returned more than one data batch","");return{item:zQ(Y),token:this._resumeToken()}}if(this._finished||this._stateToken===null)return this._finished=!0,null;let Q=await this._sendContinuation(this._stateToken,$),{batches:X}=await zX(Q),J=null,W=null;for(let Y of X){if(Y.numRows===0){let G=Y.metadata?.get(y$);if(G){W=G;continue}if(F$(Y))Y=await E$(Y,this._externalConfig,this._onLog);else{O$(Y,this._onLog);continue}}if(J!==null)throw new b("ProtocolError","A producer turn returned more than one data batch","");J=Y}if(this._stateToken=W,J===null)return this._finished=!0,null;return{item:zQ(J),token:this._resumeToken()}}seekToToken($){let{cursor:Z,callToken:Q}=D7($);this._pendingBatches=[],this._stateToken=Z,this._callStateToken=Q,this._finished=!1}async _sendContinuation($,Z){let Q=E0([]),X=new Map(Z??[]);for(let[G,H]of this._tokenMetadata($))X.set(G,H);let J=vX(Q,{},X),W=f0(Q,[J]),Y=await this._post(this._baseUrl+_8(this._prefix,this._method,{suffix:"/exchange"}),W);if(Y.status===401)throw new b("AuthenticationError","Authentication required","");return this._readResponse(Y)}close(){}}});async function C7($,Z,Q,X,J=globalThis.fetch,W=JX,Y=!1){WQ(W,"acceptedMaxResponseBytes");let G=W;if(!Y){let D=await k8($,Z,X,W,J);if(!D.acceptMaxResponseBytesSupport)throw new b("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true before RPC dispatch","");G=t$(W,D.maxResponseBytes??void 0)??W}let H=vQ(V6,{count:BigInt(Q)},_Q),z={"Content-Type":OZ};if(X)z.Authorization=X;z[bZ]=String(W);let q=await J($+j6(`${_Q}/init`,{prefix:Z}),{method:"POST",headers:z,body:H});if(q.status===404)throw new b("NotSupported","Server does not support upload URLs","");if(q.status===401)throw new b("AuthenticationError","Authentication required","");if(!q.ok)throw new b("HttpError",`__upload_url__/init failed: HTTP ${q.status}`,"");let P=CJ(q.headers);G=t$(G,P.maxResponseBytes??void 0)??G;let U=await gJ(q,G),M=await z$.from(U);await M.open();let L=[];for(let D of M.readAll()){if(D.numRows===0)continue;for(let R=0;R{JQ();WX();HZ();I8();J8();OJ();RW();H1();S6()});function V7($,Z){let Q=$.replace(/\/+$/,""),X=(Z?.prefix??"").replace(/\/+$/,""),J=Z?.onLog,W=Z?.compressionLevel,Y=Z?.authorization,G=Z?.externalLocation,H=Z?.fetch??globalThis.fetch,z=Z?.acceptedMaxResponseBytes??JX;WQ(z,"acceptedMaxResponseBytes");let q=[];function P(){return q.length===0?null:q[q.length-1]}function U(S,d){let k0=new Headers(d?.headers);if(k0.set(iG,"true"),S.token!==null)k0.set(fX,S.token);for(let[X0,h0]of Object.entries(S.echo))if(!k0.has(X0))k0.set(X0,h0);return k0}function M(S,d){let k0=d.headers.get(fX);if(k0)S.token=k0;let X0=TW.toLowerCase();if(d.headers.forEach((h0,I0)=>{if(I0.toLowerCase().startsWith(X0))S.echo[I0.slice(TW.length)]=h0}),(d.headers.get(_W)??"").trim().toLowerCase()==="true")S.token=null,S.echo={}}let L=async(S,d)=>{let k0=P();if(k0===null)return H(S,d);if(!(typeof S==="string"?S:S instanceof URL?S.href:S.url).startsWith(Q)||(d?.method??"GET").toUpperCase()==="PUT")return H(S,d);let h0=await H(S,{...d,headers:U(k0,d)});return M(k0,h0),h0},D=G?{...G,fetch:L}:G,R=Z?.description?new Map(Z.description.methods.map((S)=>[S.name,S])):null,V=Z?.description?.protocolVersion??"",g=Z?.description?.protocolName??"";function w(){if(!g)throw new b("ProtocolError","The server did not report a protocol name, so no RPC path can be built. Every request must name the protocol it addresses.","");return`${X}/${g}`}let T,E,_=!1,y=null,h=null;function s(S){let d=CJ(S.headers);if(d.maxRequestBytes!=null||d.maxResponseBytes!=null||d.uploadUrlSupport||d.acceptMaxResponseBytesSupport)y=y?{...d,maxRequestBytes:d.maxRequestBytes??y.maxRequestBytes,maxResponseBytes:d.maxResponseBytes??y.maxResponseBytes,maxUploadBytes:d.maxUploadBytes??y.maxUploadBytes}:d}async function f(){if(!h)h=k8(Q,X,Y,z,L).then((S)=>{if(!S.acceptMaxResponseBytesSupport)throw new b("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true before RPC dispatch","");y=S}).catch((S)=>{throw h=null,S});await h}function c(){return t$(z,y?.maxResponseBytes??void 0)??z}async function o(S){let d=uG(y)?y:null;if(!d)return S;if(!d.uploadUrlSupport)return S;if(d.maxRequestBytes==null||S.byteLength<=d.maxRequestBytes)return S;return j7(S,{baseUrl:Q,prefix:X,authorization:Y,urlValidator:G?.urlValidator??null,fetch:L,acceptedMaxResponseBytes:z,responseBudgetVerified:!0})}async function e(S,d){await f();let k0=await o(d),X0=await L(S,{method:"POST",headers:u(),body:await F0(k0)});if(s(X0),X0.status===413&&y?.uploadUrlSupport&&d.byteLength>0){let h0=await j7(d,{baseUrl:Q,prefix:X,authorization:Y,urlValidator:G?.urlValidator??null,fetch:L,acceptedMaxResponseBytes:z,responseBudgetVerified:!0});X0=await L(S,{method:"POST",headers:u(),body:await F0(h0)}),s(X0)}return X0}async function m(){if(_||W==null)return;try{let S=await Promise.resolve().then(() => (Y1(),UN));T=S.zstdCompress,E=S.zstdDecompress}catch{}_=!0}function u(){let S={"Content-Type":OZ};if(W!=null&&T)S["Content-Encoding"]="zstd";if(W!=null&&E)S["Accept-Encoding"]="zstd";if(S[_J]=SW(E!=null),S[bZ]=String(z),Y)S.Authorization=Y;return S}async function F0(S){if(W!=null&&T)return await T(S,W);return S}function B0(S){if(S.status===401)throw new b("AuthenticationError","Authentication required","")}async function p(S){let d=c(),k0=await gJ(S,d),X0=new Uint8Array(await gW(S.headers,k0,E,d));if(X0.byteLength>d)throw new b("TransportError",`Decoded HTTP response exceeds accepted limit (${X0.byteLength} > ${d})`,"");return X0}async function x0(){if(R)return R;await f(),await m();let S=await O1(Q,{prefix:X,protocol:Z?.protocol,externalLocation:D,authorization:Y,compressionLevel:W,compressFn:T,decompressFn:E,acceptedMaxResponseBytes:c(),fetch:L,responseBudgetVerified:!0});return R=new Map(S.methods.map((d)=>[d.name,d])),V=S.protocolVersion,g=S.protocolName,R}async function d0(S){let d=Z?.protocol??S.get(RJ);if(d)return`${X}/${d}`;return await x0(),w()}async function n0(S,d,k0,X0){let h0=await e(Q+_8(X0??w(),S,{suffix:"/init"}),d);B0(h0);let I0=await p(h0),v0=null,N$=null,lZ=null,B$=[],dZ=0,H8=(u8)=>{if(dZ+=1,dZ>1)throw new b("ProtocolError","A stream init returned more than one data batch","");B$.push(u8)},PQ=!1,f8=null;if(k0){let u8=await L1(I0),f6=await u8.readStream();if(f6)for(let I$ of f6.batches){if(I$.numRows===0){if(F$(I$)){v0=await E$(I$,D,J);continue}O$(I$,J);continue}v0=I$}let nZ=await u8.readStream();if(nZ)f8=nZ.schema;let k$=[];if(nZ)for(let I$ of nZ.batches){if(I$.numRows===0){let LX=I$.metadata?.get(y$);if(LX){N$=LX,lZ=I$.metadata?.get(YX)??lZ;continue}if(F$(I$)){H8(I$);continue}if(I$.metadata?.get(KQ)==="EXCEPTION"){k$.push(I$);continue}O$(I$,J);continue}H8(I$)}if(k$.length>0)if(B$.length>0||N$!==null)B$.push(...k$);else for(let I$ of k$)O$(I$,J);if(!nZ&&!N$)PQ=!0}else{let{schema:u8,batches:f6}=await zX(I0);f8=u8;let nZ=[];for(let k$ of f6){if(k$.numRows===0){let I$=k$.metadata?.get(y$);if(I$){N$=I$,lZ=k$.metadata?.get(YX)??lZ;continue}if(F$(k$)){H8(k$);continue}if(k$.metadata?.get(KQ)==="EXCEPTION"){nZ.push(k$);continue}O$(k$,J);continue}H8(k$)}if(nZ.length>0)if(B$.length>0||N$!==null)B$.push(...nZ);else for(let k$ of nZ)O$(k$,J)}if(B$.length===0&&N$===null)PQ=!0;let zY=(f8&&f8.fields.length>0?f8:null)??(B$.length>0?B$[0].schema:null);return{headerBatch:v0,stateToken:N$,callStateToken:lZ,pendingBatches:B$,finished:PQ,outputSchema:zY}}return{async call(S,d){await m();let X0=(await x0()).get(S);if(!X0)throw Error(`Unknown method: '${S}'`);let h0={...X0.defaults??{},...d??{}},I0=vQ(X0.paramsSchema,h0,S,{protocolVersion:V,protocol:g}),v0=await e(Q+_8(w(),S),I0);B0(v0);let N$=await p(v0),{batches:lZ}=await zX(N$),B$=null;for(let PQ of lZ){if(PQ.numRows===0)if(F$(PQ))PQ=await E$(PQ,D,J);else{O$(PQ,J);continue}if(B$!==null)throw new b("ProtocolError","A unary response returned more than one data batch","");B$=PQ}if(!B$)return null;let dZ=HQ(B$);if(dZ.length===0)return null;let H8=dZ[0];if(X0.resultSchema.fields.length===0)return null;return H8},async stream(S,d){await m();let X0=(await x0()).get(S);if(!X0)throw Error(`Unknown method: '${S}'`);let h0={...X0.defaults??{},...d??{}},I0=vQ(X0.paramsSchema,h0,S,{protocolVersion:V,protocol:g}),v0=await n0(S,I0,X0.headerSchema!=null),N$=v0.headerBatch===null?null:HQ(v0.headerBatch)[0]??null;return new B6({baseUrl:Q,prefix:w(),method:S,stateToken:v0.stateToken,callStateToken:v0.callStateToken,outputSchema:v0.outputSchema??X0.outputSchema??X0.resultSchema,inputSchema:X0.inputSchema,onLog:J,pendingBatches:v0.pendingBatches,finished:v0.finished,header:N$,rawHeader:v0.headerBatch===null?null:zQ(v0.headerBatch),compressionLevel:W,compressFn:T,decompressFn:E,authorization:Y,externalConfig:D,acceptedMaxResponseBytes:c(),postFn:e})},async callRaw(S,d){await m();let k0=dX(d),X0=await e(Q+_8(await d0(d.metadata),S),vJ(k0.schema,[k0]));B0(X0);let h0=await p(X0),{batches:I0}=await zX(h0),v0=null;for(let N$ of I0){if(N$.numRows===0)if(F$(N$))N$=await E$(N$,D,J);else{O$(N$,J);continue}if(v0!==null)throw new b("ProtocolError","A unary response returned more than one data batch","");v0=N$}return v0===null?null:zQ(v0)},async streamRaw(S,d,k0){await m();let X0=dX(d),h0=await d0(d.metadata),I0=await n0(S,vJ(X0.schema,[X0]),k0.hasHeader,h0);return new B6({baseUrl:Q,prefix:h0,method:S,stateToken:I0.stateToken,callStateToken:I0.callStateToken,outputSchema:I0.outputSchema??E0([]),onLog:J,pendingBatches:I0.pendingBatches,finished:I0.finished,header:null,rawHeader:I0.headerBatch===null?null:zQ(I0.headerBatch),compressionLevel:W,compressFn:T,decompressFn:E,authorization:Y,externalConfig:D,acceptedMaxResponseBytes:c(),postFn:e})},async resumeStream(S,d,k0){await m(),await f(),await x0();let{cursor:X0,callToken:h0}=D7(d);return new B6({baseUrl:Q,prefix:w(),method:S,stateToken:X0,callStateToken:h0,outputSchema:k0??E0([]),onLog:J,pendingBatches:[],finished:!1,header:null,compressionLevel:W,compressFn:T,decompressFn:E,authorization:Y,externalConfig:D,acceptedMaxResponseBytes:c(),postFn:e})},async describe(){return await m(),await f(),O1(Q,{prefix:X,protocol:Z?.protocol,externalLocation:D,authorization:Y,compressionLevel:W,compressFn:T,decompressFn:E,acceptedMaxResponseBytes:c(),fetch:L,responseBudgetVerified:!0})},async capabilities(){let S=await k8(Q,X,Y,z,L);return y=S,S},async requestUploadUrls(S=1){return C7(Q,X,S,Y,L,z)},beginSession(S){q.push({token:S?S:null,echo:{}})},currentSessionToken(){return P()?.token??null},currentEchoHeaders(){return{...P()?.echo??{}}},detachSession(){let S=P();if(S===null)return null;let d=S.token;return S.token=null,S.echo={},d},async endSession(){let S=q.pop();if(S===void 0)return;let{token:d,echo:k0}=S;if(d===null)return;let X0={[fX]:d};if(Y)X0.Authorization=Y;for(let[h0,I0]of Object.entries(k0))X0[h0]=I0;try{await H(Q+j6(rG,{prefix:X}),{method:"DELETE",headers:X0})}catch{}},close(){}}}var R7=O(()=>{WX();HZ();I8();BW();J8();OJ();RW();H1();F5();S6();C1();M5();O7();HU()});function hV($){let Z={resource:$.resource,authorizationServers:$.authorization_servers};if($.scopes_supported)Z.scopesSupported=$.scopes_supported;if($.bearer_methods_supported)Z.bearerMethodsSupported=$.bearer_methods_supported;if($.resource_signing_alg_values_supported)Z.resourceSigningAlgValuesSupported=$.resource_signing_alg_values_supported;if($.resource_name)Z.resourceName=$.resource_name;if($.resource_documentation)Z.resourceDocumentation=$.resource_documentation;if($.resource_policy_uri)Z.resourcePolicyUri=$.resource_policy_uri;if($.resource_tos_uri)Z.resourceTosUri=$.resource_tos_uri;if($.client_id)Z.clientId=$.client_id;if($.client_secret)Z.clientSecret=$.client_secret;if($.use_id_token_as_bearer)Z.useIdTokenAsBearer=$.use_id_token_as_bearer;if($.device_code_client_id)Z.deviceCodeClientId=$.device_code_client_id;if($.device_code_client_secret)Z.deviceCodeClientSecret=$.device_code_client_secret;return Z}async function zU($,Z){let Q=(Z??"").replace(/\/+$/,""),X=`${$.replace(/\/+$/,"")}/.well-known/oauth-protected-resource${Q}`;try{return await E7(X)}catch{return null}}async function E7($){let Z=await fetch($);if(!Z.ok)throw Error(`Failed to fetch OAuth metadata from ${$}: ${Z.status}`);let Q=await Z.json();return hV(Q)}function qU($){let Z=$.match(/^Bearer\s+(.*)/i);if(!Z)return null;let X=Z[1].match(/resource_metadata="([^"]+)"/);if(!X)return null;return X[1]}function PU($){let Z=$.match(/^Bearer\s+(.*)/i);if(!Z)return null;let X=Z[1].match(/client_id="([^"]+)"/);if(!X)return null;return X[1]}function FU($){let Z=$.match(/^Bearer\s+(.*)/i);if(!Z)return null;let X=Z[1].match(/client_secret="([^"]+)"/);if(!X)return null;return X[1]}function NU($){let Z=$.match(/^Bearer\s+(.*)/i);if(!Z)return!1;let X=Z[1].match(/use_id_token_as_bearer="([^"]+)"/);if(!X)return!1;return X[1]==="true"}function UU($){let Z=$.match(/^Bearer\s+(.*)/i);if(!Z)return null;let X=Z[1].match(/device_code_client_id="([^"]+)"/);if(!X)return null;return X[1]}function MU($){let Z=$.match(/^Bearer\s+(.*)/i);if(!Z)return null;let X=Z[1].match(/device_code_client_secret="([^"]+)"/);if(!X)return null;return X[1]}function fV(){if(lW)return lW;let $=globalThis.process?.getBuiltinModule;if(typeof $==="function"){let X=$.call(globalThis.process,LU);if(X?.writeSync)return lW=X.writeSync.bind(X),lW}let Z=import.meta.require??globalThis.require??null;if(!Z)throw Error("IpcStreamWriter needs synchronous node:fs.writeSync, reached via import.meta.require (Bun), globalThis.require (Node CJS), or process.getBuiltinModule (Node >= 20.16). This runtime offers none of them, so the subprocess transport is unavailable. On an older Node ESM, either upgrade or set globalThis.require = createRequire(import.meta.url).");let Q=Z(LU);return lW=Q.writeSync.bind(Q),lW}function AU($,Z){let Q=fV(),X=0,J=0;while(X{let W=()=>{$.off("drain",Y),$.off("error",G),$.off("close",H)},Y=()=>{W(),X()},G=(z)=>{W(),J(z)},H=()=>{W(),X()};$.once("drain",Y),$.once("error",G),$.once("close",H)})}class S7{target;constructor($=bV){if(typeof $==="number")this.target={kind:"fd",fd:$};else if(typeof $.write==="function"&&!("writable"in $))this.target={kind:"sink",sink:$};else this.target={kind:"socket",socket:$}}async writeStream($,Z){let Q=N6($,Z);if(this.target.kind==="fd")AU(this.target.fd,Q);else if(this.target.kind==="sink")await this.target.sink.write(Q);else await DU(this.target.socket,Q)}openStream($){return new OU(this.target,$)}}class OU{encoder;target;closed=!1;writeChain=Promise.resolve();constructor($,Z){this.target=$,this.encoder=F6(Z),this.enqueue(this.encoder.start())}async write($){if(this.closed)throw Error("Stream already closed");return this.enqueue(this.encoder.writeBatch($))}async close(){if(this.closed)return;return this.closed=!0,this.enqueue(this.encoder.finish())}enqueue($){let Z=this.target;if(Z.kind==="fd")return AU(Z.fd,$),mV;let Q=this.writeChain.then(()=>{if(Z.kind==="sink")return Z.sink.write($);return DU(Z.socket,$)});return this.writeChain=Q.catch(()=>{return}),Q}}var bV=1,mV,LU="node:fs",lW=null,uV=1073741824,w7=131072;var B7=O(()=>{qZ();mV=Promise.resolve()});function jU($,Z){if($.name!==Z.name||$.nullable!==Z.nullable||String($.type)!==String(Z.type))return!1;let Q=$.type.children??[],X=Z.type.children??[];return Q.length===X.length&&Q.every((J,W)=>jU(J,X[W]))}function CU($,Z){return $.fields.length===Z.fields.length&&$.fields.every((Q,X)=>jU(Q,Z.fields[X]))}class dW{encoder;writeFn;closed=!1;constructor($,Z){this.writeFn=$,this.encoder=KU(Z)}write($){if(this.closed)throw Error("PipeIncrementalWriter already closed");this.emit(this.encoder.writeBatch($))}close(){if(this.closed)return;this.closed=!0,this.emit(this.encoder.finish())}emit($){if($.length>0)this.writeFn($)}}function k7($,Z,Q){let X=Q?.onLog,J=Q?.externalLocation,W=null,Y=null,G=null,H="",z="",q="",P=[],U,M,L=Q?.protocol,D=!1,R=null,V=!1,g=(h)=>{let s=0;do{let f=Math.min(s+w7,h.length);Z.write(h.subarray(s,f)),s=f}while(s{g(A7(o,e));let u=await(await w()).readStream();if(!u)throw new b("TransportError",`EOF reading the '${o}' reflection response`,"");return L7(u.batches,X,J)},s,f=L;if(!f)s=K5(await h(N1)),f=D1(s);let c=A1(H5(await h(w6,f)),s);return H=c.protocolName,z=c.protocolVersion,q=c.protocolHash,P=c.hostedProtocols,U=c.serverId,M=c.requestVersion,G=new Map(c.methods.map((o)=>[o.name,o])),G}finally{E()}}return{async call(h,s){let f=await y();await T();try{let c=f.get(h);if(!c)throw Error(`Unknown method: '${h}'`);let o=await w(),e={...c.defaults??{},...s??{}},m=vQ(c.paramsSchema,e,h,{protocolVersion:z,protocol:H});g(m);let u=await o.readStream();if(!u)throw Error("EOF reading response");let F0=null;for(let p of u.batches){if(p.numRows===0)if(F$(p))p=await E$(p,J,X);else{O$(p,X);continue}if(F0!==null)throw new b("ProtocolError","A unary response returned more than one data batch","");F0=p}if(!F0)return null;let B0=HQ(F0);if(B0.length===0)return null;if(c.resultSchema.fields.length===0)return null;return B0[0]}finally{E()}},async callRaw(h,s){await T();try{let f=dX(s);g(vJ(f.schema,[f]));let o=await(await w()).readStream();if(!o)throw new b("TransportError","EOF reading response","");let e=null;for(let m of o.batches){if(m.numRows===0)if(F$(m))m=await E$(m,J,X);else{O$(m,X);continue}if(e!==null)throw new b("ProtocolError","A unary response returned more than one data batch","");e=m}return e===null?null:zQ(e)}finally{E()}},async streamRaw(h,s,f){await T();try{let c=dX(s);g(vJ(c.schema,[c]));let o=null;if(f.hasHeader){let e=await(await w()).readStream();if(e)for(let m of e.batches){if(m.numRows===0){if(F$(m)){o??=zQ(await E$(m,J,X));continue}O$(m,X);continue}o??=zQ(m)}}return new j1({reader:w,writeFn:g,onLog:X,header:null,rawHeader:o,outputSchema:E0([]),releaseBusy:E,setDrainPromise:_,externalConfig:J})}catch(c){try{let o=await w();g(f0(E0([]),[])),await o.readStream()}catch{}throw E(),c}},async stream(h,s){let f=await y();await T();try{let c=f.get(h);if(!c)throw Error(`Unknown method: '${h}'`);let o=await w(),e={...c.defaults??{},...s??{}},m=vQ(c.paramsSchema,e,h,{protocolVersion:z,protocol:H});g(m);let u=null;if(c.headerSchema){let B0=await o.readStream();if(B0)for(let p of B0.batches){if(p.numRows===0)if(F$(p))p=await E$(p,J,X);else{O$(p,X);continue}let x0=HQ(p);if(x0.length>0)u=x0[0]}}let F0=c.outputSchema??c.resultSchema;return new j1({reader:o,writeFn:g,onLog:X,header:u,outputSchema:F0,releaseBusy:E,setDrainPromise:_,externalConfig:J})}catch(c){try{let o=await w(),e=E0([]),m=f0(e,[]);g(m);let u=await o.readStream()}catch{}throw E(),c}},async describe(){let h=await y();return{protocolName:H,protocolVersion:z,protocolHash:q,hostedProtocols:P,methods:[...h.values()],serverId:U,requestVersion:M}},close(){if(V)return;V=!0,Z.end()}}}function VU($,Z){let Q=Bun.spawn($,{stdin:"pipe",stdout:"pipe",stderr:Z?.stderr??"ignore",cwd:Z?.cwd,env:Z?.env?{...process.env,...Z.env}:void 0}),X=Q.stdout,W=k7(X,{write(G){Q.stdin.write(G)},flush(){Q.stdin.flush()},end(){Q.stdin.end()}},{onLog:Z?.onLog,externalLocation:Z?.externalLocation}),Y=W.close;return W.close=()=>{Y.call(W);try{Q.kill()}catch{}},W}var j1;var RU=O(()=>{WX();HZ();I8();J8();M1();q5();B7();F5();S6();C1();M5();j1=class j1{_openReader;_readerCache;_writeFn;_onLog;_header;_rawHeader;_inputWriter=null;_inputSchema=null;_outputStreamOpened=!1;_closed=!1;_outputSchema;_releaseBusy;_setDrainPromise;_externalConfig;constructor($){this._openReader=typeof $.reader==="function"?$.reader:async()=>$.reader,this._readerCache=typeof $.reader==="function"?null:$.reader,this._writeFn=$.writeFn,this._onLog=$.onLog,this._header=$.header,this._rawHeader=$.rawHeader??null,this._outputSchema=$.outputSchema,this._releaseBusy=$.releaseBusy,this._setDrainPromise=$.setDrainPromise,this._externalConfig=$.externalConfig}get header(){return this._header}get rawHeader(){return this._rawHeader}async _reader(){return this._readerCache??=await this._openReader(),this._readerCache}async _readOutputBatch(){while(!0){let $=await(await this._reader()).readNextBatch();if($===null)return null;if($.numRows===0){if(F$($))return await E$($,this._externalConfig,this._onLog);if(O$($,this._onLog))continue}return $}}async _ensureOutputStream(){if(this._outputStreamOpened)return;if(this._outputStreamOpened=!0,!await(await this._reader()).openNextStream())throw new b("ProtocolError","Expected output stream but got EOF","")}async tick($){let Z=await this._tickBatch($);return Z===null?[]:HQ(Z)}async tickRaw($){let Z=await this._tickBatch($);return Z===null?null:zQ(Z)}async nextWithTokenRaw(){let $=await this.tickRaw();return $===null?null:{item:$,token:null}}async _tickBatch($){if(this._closed)throw new b("ProtocolError","Stream session is closed","");let Z=E0([]);if(!this._inputWriter)this._inputWriter=new dW(this._writeFn,Z);let Q=GQ(Z,$?new Map($):void 0);this._inputWriter.write(Q),await this._ensureOutputStream();let X;try{X=await this._readOutputBatch()}catch(J){throw await this._cleanup(),J}if(X===null)return this._closed=!0,this._inputWriter.close(),this._inputWriter=null,this._releaseBusy(),null;return X}async exchangeRaw($){if(this._closed)throw new b("ProtocolError","Stream session is closed","");let Z=dX($),Q=Z.schema;if(this._inputSchema&&!CU(this._inputSchema,Q))throw new b("ProtocolError",`Exchange input schema changed: expected ${this._inputSchema}, got ${Q}`,"");if(this._inputSchema??=Q,!this._inputWriter)this._inputWriter=new dW(this._writeFn,Q);this._inputWriter.write(Z),await this._ensureOutputStream();try{let X=await this._readOutputBatch();return X===null?null:zQ(X)}catch(X){throw await this._cleanup(),X}}async cancel(){if(this._closed)return;this._closed=!0;let $=new Map([[bX,"1"]]);try{let Z=this._inputSchema??E0([]);if(!this._inputWriter)this._inputWriter=new dW(this._writeFn,Z);this._inputWriter.write(GQ(Z,$)),this._inputWriter.close(),this._inputWriter=null}catch{this._releaseBusy();return}try{if(!this._outputStreamOpened){if(this._outputStreamOpened=!0,!await(await this._reader()).openNextStream()){this._releaseBusy();return}}while(await(await this._reader()).readNextBatch()!==null);}catch{}this._releaseBusy()}async exchange($){if(this._closed)throw new b("ProtocolError","Stream session is closed","");let Z,Q;if(!Array.isArray($)){if(Z=$.schema,Q=$,this._inputSchema&&!CU(this._inputSchema,Z))throw new b("ProtocolError",`Exchange input schema changed: expected ${this._inputSchema}, got ${Z}`,"");this._inputSchema??=Z}else if($.length===0)Z=this._inputSchema??this._outputSchema,Q=GQ(Z);else{let J=Object.keys($[0]).map((Y)=>{let G;for(let z of $)if(z[Y]!=null){G=z[Y];break}let H=P5(G);return O0(Y,H,!0)});if(Z=E0(J),this._inputSchema){let Y=this._inputSchema;if(Y.fields.length!==Z.fields.length||Y.fields.some((G,H)=>G.name!==Z.fields[H].name))throw new b("ProtocolError",`Exchange input schema changed: expected [${Y.fields.map((G)=>G.name).join(", ")}] but got [${Z.fields.map((G)=>G.name).join(", ")}]`,"")}else this._inputSchema=Z;let W={};for(let Y of Z.fields)W[Y.name]=$.map((G)=>G[Y.name]);Q=YQ(Z,W)}if(!this._inputWriter)this._inputWriter=new dW(this._writeFn,Z);this._inputWriter.write(Q),await this._ensureOutputStream();try{let X=await this._readOutputBatch();if(X===null)return[];return HQ(X)}catch(X){throw await this._cleanup(),X}}async _cleanup(){if(this._closed)return;if(this._closed=!0,this._inputWriter)this._inputWriter.close(),this._inputWriter=null;try{if(this._outputStreamOpened)while(await(await this._reader()).readNextBatch()!==null);}catch{}this._releaseBusy()}async*[Symbol.asyncIterator](){if(this._closed)return;try{let $=E0([]);this._inputWriter=new dW(this._writeFn,$);while(!0){let Z=await this.tick();if(this._closed)break;yield Z}}finally{if(this._inputWriter)this._inputWriter.close(),this._inputWriter=null;try{if(this._outputStreamOpened)while(await(await this._reader()).readNextBatch()!==null);}catch{}this._closed=!0,this._releaseBusy()}}close(){if(this._closed)return;if(this._closed=!0,this._inputWriter)this._inputWriter.close(),this._inputWriter=null;else{let Z=E0([]),Q=f0(Z,[]);this._writeFn(Q)}let $=(async()=>{try{if(!this._outputStreamOpened){if(await(await this._reader()).openNextStream())while(await(await this._reader()).readNextBatch()!==null);}else while(await(await this._reader()).readNextBatch()!==null);}catch{}finally{this._releaseBusy()}})();this._setDrainPromise($)}}});var EU=O(()=>{RW();R7();F5();RU();O7()});function V1($){let Z={resource:$.resource,authorization_servers:$.authorizationServers};if($.scopesSupported)Z.scopes_supported=$.scopesSupported;if($.bearerMethodsSupported)Z.bearer_methods_supported=$.bearerMethodsSupported;if($.resourceSigningAlgValuesSupported)Z.resource_signing_alg_values_supported=$.resourceSigningAlgValuesSupported;if($.resourceName)Z.resource_name=$.resourceName;if($.resourceDocumentation)Z.resource_documentation=$.resourceDocumentation;if($.resourcePolicyUri)Z.resource_policy_uri=$.resourcePolicyUri;if($.resourceTosUri)Z.resource_tos_uri=$.resourceTosUri;if($.clientId){if(!/^[A-Za-z0-9\-._~]+$/.test($.clientId))throw Error("Invalid client_id: must contain only URL-safe characters [A-Za-z0-9\\-._~]");Z.client_id=$.clientId}if($.clientSecret){if(!/^[A-Za-z0-9\-._~]+$/.test($.clientSecret))throw Error("Invalid client_secret: must contain only URL-safe characters [A-Za-z0-9\\-._~]");Z.client_secret=$.clientSecret}if($.deviceCodeClientId){if(!/^[A-Za-z0-9\-._~]+$/.test($.deviceCodeClientId))throw Error("Invalid device_code_client_id: must contain only URL-safe characters [A-Za-z0-9\\-._~]");Z.device_code_client_id=$.deviceCodeClientId}if($.deviceCodeClientSecret){if(!/^[A-Za-z0-9\-._~]+$/.test($.deviceCodeClientSecret))throw Error("Invalid device_code_client_secret: must contain only URL-safe characters [A-Za-z0-9\\-._~]");Z.device_code_client_secret=$.deviceCodeClientSecret}if($.useIdTokenAsBearer)Z.use_id_token_as_bearer=!0;return Z}function I7($){return`/.well-known/oauth-protected-resource${$}`}function wU($,Z,Q,X,J,W){let Y="Bearer";if($)Y+=` resource_metadata="${$}"`;if(Z)Y+=`, client_id="${Z}"`;if(Q)Y+=`, client_secret="${Q}"`;if(J)Y+=`, device_code_client_id="${J}"`;if(W)Y+=`, device_code_client_secret="${W}"`;if(X)Y+=', use_id_token_as_bearer="true"';return Y}function _7($){let Z=$?.vgiAuthReason,Q=$ instanceof Error?$.message:"";if(typeof Z==="string"&&cV.has(Z))return{reason:Z,detail:$ instanceof qQ?$.detail:Q};if($ instanceof Error&&$.name==="PermissionError")return{reason:CZ.InsufficientScope,detail:Q};return{reason:CZ.Unauthorized,detail:Q}}function T7($){let Z=[...new Set($)];if(Z.length===0)return"";return`This service only accepts requests that arrive through its configured reverse proxy, which must set the ${Z.join(", ")} header(s). A rejection here is at least as likely `+"to be a proxy misconfiguration as a bad credential — check that the proxy is forwarding "+"them before re-issuing credentials."}function g7($,Z,Q=""){let X={error:"unauthorized",reason:$,detail:Z};if(Q)X.proxy_hint=Q;return JSON.stringify(X)}var nW="VGI-Auth-Reason",oW="VGI-Auth-Proxy-Required",CZ,cV,qQ,qX;var R1=O(()=>{CZ={MissingCredential:"missing_credential",InvalidCredential:"invalid_credential",ExpiredCredential:"expired_credential",InsufficientScope:"insufficient_scope",ProxyRequired:"proxy_required",Unauthorized:"unauthorized"},cV=new Set(Object.values(CZ));qQ=class qQ extends Error{reason;detail;vgiAuthReason;constructor($,Z=""){super(Z||$);this.name="AuthFailure",this.reason=$,this.detail=Z,this.vgiAuthReason=$}};qX=class qX extends Error{retryAfter;detail;constructor($="",Z=5){super($||"authentication service unavailable");this.name="AuthUnavailableError",this.detail=$,this.retryAfter=Z}}});function L5($){let{validate:Z}=$;return async function(X){let J=X.headers.get("Authorization")??"";if(!J)throw new qQ(CZ.MissingCredential,"Missing Authorization header");if(!J.startsWith("Bearer "))throw new qQ(CZ.InvalidCredential,"Authorization header is not a Bearer credential");let W=J.slice(7);return Z(W)}}function lV($,Z){let Q=new TextEncoder;return uN(Q.encode($),Q.encode(Z))}function x7($){let Z=$.tokens instanceof Map?[...$.tokens.entries()]:Object.entries($.tokens);function Q(X){for(let[J,W]of Z)if(lV(X,J))return W;throw new qQ(CZ.InvalidCredential,"Unknown bearer token")}return L5({validate:Q})}function dV($){if($ instanceof qX)return!1;if($ instanceof qQ)return!0;return $ instanceof Error&&$.constructor===Error&&$.name!=="PermissionError"}function nV($){if($.length===0)return CZ.Unauthorized;return $.find((Q)=>Q!==CZ.MissingCredential)??CZ.MissingCredential}function E1(...$){if($.length===0)throw Error("chainAuthenticate requires at least one authenticator");return async function(Q){let X=null,J=[];for(let Y of $)try{return await Y(Q)}catch(G){if(dV(G)){X=G,J.push(G instanceof qQ?G.reason:CZ.Unauthorized);continue}throw G}let W=new qQ(nV(J),"No authenticator accepted the request");if(X)W.cause=X;throw W}}var v7=O(()=>{R6();R1()});function SU(){return typeof navigator<"u"&&navigator.userAgent==="Cloudflare-Workers"}function A5($){return gH($)||xH($)||vH($)||yH($)||TH($)||kH($)||IH($)||bH($)||mH($)}var BU=O(()=>{qZ()});function k6($,Z,Q){let X=$.fields,J=Z.fields;if(X.length!==J.length)throw new b("ProtocolError",`Parameter schema mismatch for method '${Q}': expected ${J.length} fields, got ${X.length}.`,"");for(let W=0;W0&&Z.numRows!==1)throw new b("ProtocolError",`Expected 1 row in request batch, got ${Z.numRows}. Each parameter is a column (not a row). The batch should have exactly 1 row.`,"");let G=aY.opaquePassthrough;for(let H=0;H<$.fields.length;H++){let z=$.fields[H];if(G&&(hH(z.type)||A5(z.type))){let P=Z.getChildAt(H);Y[z.name]=P.data?.[0]??P.get(0);continue}let q=Z.getChildAt(H)?.get(0);if(q instanceof Uint32Array&&A5(z.type))try{q=BigInt(q.toString())}catch{}if(typeof q==="bigint"&&!A5(z.type)){if(q>=oV&&q<=sV)q=Number(q)}Y[z.name]=q}return{methodName:X,protocol:Q.get(RJ)??"",requestVersion:J,requestId:W,schema:$,params:Y,rawMetadata:Q}}function w1($,Z){if(!Z)return $;for(let Q of Object.keys(Z))if($[Q]==null)$[Q]=Z[Q];return $}var oV,sV;var S1=O(()=>{qZ();HZ();BU();oV=BigInt(Number.MIN_SAFE_INTEGER),sV=BigInt(Number.MAX_SAFE_INTEGER)});function I6($){return eY($)}var kU=O(()=>{qZ()});function y7($){return $ instanceof Uint8Array||ArrayBuffer.isView($)&&$.constructor.name==="Uint8Array"&&"BYTES_PER_ELEMENT"in $&&$.BYTES_PER_ELEMENT===1}function O5($,Z=""){if(typeof $!=="boolean")throw TypeError(D5(Z)+"expected boolean, got type="+typeof $);return $}function _6($,Z=""){if(typeof $!=="number")throw TypeError(D5(Z)+"expected number, got "+typeof $);if(!Number.isSafeInteger($)||$<0)throw RangeError(D5(Z)+"expected integer >= 0, got "+$);return $}function b$($,Z,Q=""){if(y7($)&&(Z===void 0||$.length===Z))return $;if(Z!==void 0)_6(Z,"length");let X=y7($),J=Z!==void 0?` of length ${Z}`:"",W=X?`length=${$.length}`:`type=${typeof $}`,Y=D5(Q)+"expected Uint8Array"+J+", got "+W;if(!X)throw TypeError(Y);throw RangeError(Y)}function h7($,Z=!0){if($.destroyed)throw Error("hash was destroyed");if(Z&&$.finished)throw Error("digest() was already called")}function _U($,Z){b$($,void 0,"output");let Q=Z.outputLen;if(!($.length>=Q))throw RangeError('"output" expected length >= '+Q)}function PX($){return new Uint32Array($.buffer,$.byteOffset,Math.floor($.byteLength/4))}function yQ(...$){for(let Z=0;Z<$.length;Z++)$[Z].fill(0)}function iV($){return new DataView($.buffer,$.byteOffset,$.byteLength)}function rV($){return $<<24&4278190080|$<<8&16711680|$>>>8&65280|$>>>24&255}function aV($){for(let Z=0;Z<$.length;Z++)$[Z]=rV($[Z]);return $}function TU($,Z){return IU($,"defaults"),IU(Z,"opts"),Object.assign($,Z)}function gU($,Z){if($=b$($),Z=b$(Z),$.length!==Z.length)return!1;let Q=0;for(let X=0;X<$.length;X++)Q|=$[X]^Z[X];return Q===0}function xU($,Z,Q){let X=Z,J=Q||(()=>[]),W=(G,H)=>X(H,...J(G)).update(G).digest(),Y=X(new Uint8Array($),...J(new Uint8Array(0)));return W.outputLen=Y.outputLen,W.blockLen=Y.blockLen,W.create=(G,...H)=>X(G,...H),W}function B1($,Z,Q=!0){if(Z===void 0)return new Uint8Array($);if(b$(Z,$,"output"),Q&&!k1(Z))throw Error("invalid output, must be aligned");return Z}function yU($,Z,Q){_6($),_6(Z),O5(Q);let X=new Uint8Array(16),J=iV(X);return J.setBigUint64(0,BigInt(Z),Q),J.setBigUint64(8,BigInt($),Q),X}function k1($){return $.byteOffset%4===0}function sW($){return Uint8Array.from(b$($))}var D5=($)=>$?`"${$}" `:"",IU=($,Z)=>{if($===null||typeof $!=="object"||Array.isArray($))throw TypeError(Z==="object"?"expected valid options object":`"${Z}" expected object, got type=${typeof $}`)},Y8,hQ,vU=($,Z)=>{function Q(X,...J){if(b$(X,void 0,"key"),$.nonceLength!==void 0){let P=J[0];b$(P,$.varSizeNonce?void 0:$.nonceLength,"nonce")}let W=$.tagLength,Y=$.nonceLength!==void 0?1:0;if(!$.withAAD){for(let P=Y;P{if(U!==void 0){if(P!==2)throw Error("cipher output not supported");b$(U,void 0,"output")}},z=!1;return{encrypt(P,U){if(z)throw Error("cannot encrypt() twice with same key + nonce");return z=!0,b$(P,void 0,"data"),H(G.encrypt.length,U),G.encrypt(P,U)},decrypt(P,U){if(b$(P,void 0,"data"),W&&P.length= tagLength='+W);return H(G.decrypt.length,U),G.decrypt(P,U)}}}return Object.assign(Q,$),Q};var C5=O(()=>{/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */Y8=(()=>new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68)();hQ=Y8?($)=>$:aV});function l0($,Z){return $<>>32-Z}function ZR($,Z,Q,X,J,W,Y,G){let H=J.length,z=new Uint8Array(I1),q=PX(z),P=Y8&&k1(J)&&k1(W),U=P?PX(J):hU,M=P?PX(W):hU;if(!Y8){for(let L=0;L=b7)throw Error("arx: counter overflow");let D=Math.min(I1,H-L);for(let R=0,V;R=b7)throw Error("arx: counter overflow");let D=Math.min(I1,H-L);if(P&&D===I1){let R=L/4;if(L%4!==0)throw Error("arx: invalid block position");for(let V=0,g;V<$R;V++)g=R+V,M[g]=U[g]^q[V];L+=I1;continue}for(let R=0,V;R{b$(G,void 0,"key"),b$(H,void 0,"nonce"),b$(z,void 0,"data");let U=z.length;if(q=B1(U,q,!1),_6(P),P<0||P>=b7)throw Error("arx: counter overflow");let M=[],L=G.length,D,R;if(L===32)M.push(D=sW(G)),R=eV;else if(L===16&&Q)D=new Uint8Array(32),D.set(G),D.set(G,16),R=tV,M.push(D);else throw b$(G,32,"arx key"),Error("invalid key size");if(!Y8||!k1(H))M.push(H=sW(H));let V=PX(D);if(X){if(H.length!==24)throw Error("arx: extended nonce must be 24 bytes");let T=H.subarray(0,16);if(Y8)X(R,V,PX(T),V);else{let E=hQ(Uint32Array.from(R));X(E,V,PX(T),V),yQ(E),hQ(V)}H=H.subarray(16)}else if(!Y8)hQ(V);let g=16-J;if(g!==H.length)throw Error(`arx: nonce must be ${g} or 16 bytes`);if(g!==12){let T=new Uint8Array(12);T.set(H,W?0:12-H.length),H=T,M.push(H)}let w=hQ(PX(H));try{return ZR($,R,V,w,z,q,P,Y),q}finally{yQ(...M)}}}var bU=($)=>Uint8Array.from($.split(""),(Z)=>Z.charCodeAt(0)),tV,eV,I1=64,$R=16,b7,hU;var fU=O(()=>{C5();tV=(()=>hQ(PX(bU("expand 16-byte k"))))(),eV=(()=>hQ(PX(bU("expand 32-byte k"))))();b7=(()=>4294967295)(),hU=Uint32Array.of()});function jZ($,Z){return $[Z++]&255|($[Z++]&255)<<8}class uU{blockLen=16;outputLen=16;buffer=new Uint8Array(16);r=new Uint16Array(10);h=new Uint16Array(10);pad=new Uint16Array(8);pos=0;finished=!1;destroyed=!1;constructor($){$=sW(b$($,32,"key"));let Z=jZ($,0),Q=jZ($,2),X=jZ($,4),J=jZ($,6),W=jZ($,8),Y=jZ($,10),G=jZ($,12),H=jZ($,14);this.r[0]=Z&8191,this.r[1]=(Z>>>13|Q<<3)&8191,this.r[2]=(Q>>>10|X<<6)&7939,this.r[3]=(X>>>7|J<<9)&8191,this.r[4]=(J>>>4|W<<12)&255,this.r[5]=W>>>1&8190,this.r[6]=(W>>>14|Y<<2)&8191,this.r[7]=(Y>>>11|G<<5)&8065,this.r[8]=(G>>>8|H<<8)&8191,this.r[9]=H>>>5&127;for(let z=0;z<8;z++)this.pad[z]=jZ($,16+2*z)}process($,Z,Q=!1){let X=Q?0:2048,{h:J,r:W}=this,Y=W[0],G=W[1],H=W[2],z=W[3],q=W[4],P=W[5],U=W[6],M=W[7],L=W[8],D=W[9],R=jZ($,Z+0),V=jZ($,Z+2),g=jZ($,Z+4),w=jZ($,Z+6),T=jZ($,Z+8),E=jZ($,Z+10),_=jZ($,Z+12),y=jZ($,Z+14),h=J[0]+(R&8191),s=J[1]+((R>>>13|V<<3)&8191),f=J[2]+((V>>>10|g<<6)&8191),c=J[3]+((g>>>7|w<<9)&8191),o=J[4]+((w>>>4|T<<12)&8191),e=J[5]+(T>>>1&8191),m=J[6]+((T>>>14|E<<2)&8191),u=J[7]+((E>>>11|_<<5)&8191),F0=J[8]+((_>>>8|y<<8)&8191),B0=J[9]+(y>>>5|X),p=0,x0=p+h*Y+s*(5*D)+f*(5*L)+c*(5*M)+o*(5*U);p=x0>>>13,x0&=8191,x0+=e*(5*P)+m*(5*q)+u*(5*z)+F0*(5*H)+B0*(5*G),p+=x0>>>13,x0&=8191;let d0=p+h*G+s*Y+f*(5*D)+c*(5*L)+o*(5*M);p=d0>>>13,d0&=8191,d0+=e*(5*U)+m*(5*P)+u*(5*q)+F0*(5*z)+B0*(5*H),p+=d0>>>13,d0&=8191;let n0=p+h*H+s*G+f*Y+c*(5*D)+o*(5*L);p=n0>>>13,n0&=8191,n0+=e*(5*M)+m*(5*U)+u*(5*P)+F0*(5*q)+B0*(5*z),p+=n0>>>13,n0&=8191;let S=p+h*z+s*H+f*G+c*Y+o*(5*D);p=S>>>13,S&=8191,S+=e*(5*L)+m*(5*M)+u*(5*U)+F0*(5*P)+B0*(5*q),p+=S>>>13,S&=8191;let d=p+h*q+s*z+f*H+c*G+o*Y;p=d>>>13,d&=8191,d+=e*(5*D)+m*(5*L)+u*(5*M)+F0*(5*U)+B0*(5*P),p+=d>>>13,d&=8191;let k0=p+h*P+s*q+f*z+c*H+o*G;p=k0>>>13,k0&=8191,k0+=e*Y+m*(5*D)+u*(5*L)+F0*(5*M)+B0*(5*U),p+=k0>>>13,k0&=8191;let X0=p+h*U+s*P+f*q+c*z+o*H;p=X0>>>13,X0&=8191,X0+=e*G+m*Y+u*(5*D)+F0*(5*L)+B0*(5*M),p+=X0>>>13,X0&=8191;let h0=p+h*M+s*U+f*P+c*q+o*z;p=h0>>>13,h0&=8191,h0+=e*H+m*G+u*Y+F0*(5*D)+B0*(5*L),p+=h0>>>13,h0&=8191;let I0=p+h*L+s*M+f*U+c*P+o*q;p=I0>>>13,I0&=8191,I0+=e*z+m*H+u*G+F0*Y+B0*(5*D),p+=I0>>>13,I0&=8191;let v0=p+h*D+s*L+f*M+c*U+o*P;p=v0>>>13,v0&=8191,v0+=e*q+m*z+u*H+F0*G+B0*Y,p+=v0>>>13,v0&=8191,p=(p<<2)+p|0,p=p+x0|0,x0=p&8191,p=p>>>13,d0+=p,J[0]=x0,J[1]=d0,J[2]=n0,J[3]=S,J[4]=d,J[5]=k0,J[6]=X0,J[7]=h0,J[8]=I0,J[9]=v0}finalize(){let{h:$,pad:Z}=this,Q=new Uint16Array(10),X=$[1]>>>13;$[1]&=8191;for(let Y=2;Y<10;Y++)$[Y]+=X,X=$[Y]>>>13,$[Y]&=8191;$[0]+=X*5,X=$[0]>>>13,$[0]&=8191,$[1]+=X,X=$[1]>>>13,$[1]&=8191,$[2]+=X,Q[0]=$[0]+5,X=Q[0]>>>13,Q[0]&=8191;for(let Y=1;Y<10;Y++)Q[Y]=$[Y]+X,X=Q[Y]>>>13,Q[Y]&=8191;Q[9]-=8192;let J=(X^1)-1;for(let Y=0;Y<10;Y++)Q[Y]&=J;J=~J;for(let Y=0;Y<10;Y++)$[Y]=$[Y]&J|Q[Y];$[0]=($[0]|$[1]<<13)&65535,$[1]=($[1]>>>3|$[2]<<10)&65535,$[2]=($[2]>>>6|$[3]<<7)&65535,$[3]=($[3]>>>9|$[4]<<4)&65535,$[4]=($[4]>>>12|$[5]<<1|$[6]<<14)&65535,$[5]=($[6]>>>2|$[7]<<11)&65535,$[6]=($[7]>>>5|$[8]<<8)&65535,$[7]=($[8]>>>8|$[9]<<5)&65535;let W=$[0]+Z[0];$[0]=W&65535;for(let Y=1;Y<8;Y++)W=($[Y]+Z[Y]|0)+(W>>>16)|0,$[Y]=W&65535;yQ(Q)}update($){h7(this),b$($),$=sW($);let{buffer:Z,blockLen:Q}=this,X=$.length;for(let J=0;J>>0,$[J++]=Q[W]>>>8}digest(){let{buffer:$,outputLen:Z}=this;this.digestInto($);let Q=$.slice(0,Z);return this.destroy(),Q}}var pU;var cU=O(()=>{C5();pU=xU(32,($)=>new uU($))});function nU($,Z,Q,X,J,W=20){let Y=$[0],G=$[1],H=$[2],z=$[3],q=Z[0],P=Z[1],U=Z[2],M=Z[3],L=Z[4],D=Z[5],R=Z[6],V=Z[7],g=J,w=Q[0],T=Q[1],E=Q[2],_=Y,y=G,h=H,s=z,f=q,c=P,o=U,e=M,m=L,u=D,F0=R,B0=V,p=g,x0=w,d0=T,n0=E;for(let d=0;d{$.update(Z);let Q=Z.length%16;if(Q)$.update(JR.subarray(Q))},WR,YR=($)=>(Z,Q,X)=>{return{encrypt(W,Y){let G=W.length;Y=B1(G+16,Y,!1),Y.set(W);let H=Y.subarray(0,-16);$(Z,Q,H,H,1);let z=dU($,Z,Q,H,X);return Y.set(z,G),yQ(z),Y},decrypt(W,Y){Y=B1(W.length-16,Y,!1);let G=W.subarray(0,-16),H=W.subarray(-16),z=dU($,Z,Q,G,X);if(!gU(H,z))throw yQ(z),Error("invalid tag");return Y.set(W.subarray(0,-16)),$(Z,Q,Y,Y,1),yQ(z),Y}}},m7;var oU=O(()=>{fU();cU();C5();XR=mU(nU,{counterRight:!1,counterLength:8,extendNonceFn:QR,allowShortKeys:!1}),JR=new Uint8Array(16),WR=new Uint8Array(32);m7=vU({blockSize:64,nonceLength:24,tagLength:16,withAAD:!0},YR(XR))});function _1($,Z,Q){if(Z.length!==32)throw Error("AEAD key must be 32 bytes — call normalizeKey() first");let X=Q.version??1;if(X<1||X>255)throw Error(`AEAD envelope version must fit in one byte; got ${X}`);let J=hW(iW),W=m7(Z,J,Q.aad).encrypt($),Y=new Uint8Array(T6+iW+W.length);return Y[0]=X,Y.set(J,T6),Y.set(W,T6+iW),Y}function T1($,Z,Q){if(Z.length!==32)throw Error("AEAD key must be 32 bytes — call normalizeKey() first");if($.length{oU();R6();KR=T6+iW+GR;nX=class nX extends Error{constructor($){super($);this.name="SealError"}}});function g1($){return $.evidenceBinding?rU(zR,$,$.evidenceBinding):aU(HR,$)}function sU($){return $.evidenceBinding?rU(PR,$,$.evidenceBinding):aU(qR,$)}function iU($){return x8.encode(`\x00${$}`)}function rU($,Z,Q){let X=x8.encode(Q),J=iU(Z.protocol);if(Z.principal===null||Z.principal===void 0)return R5($,x8.encode("\x00anonymous\x00"),X,J);return R5($,new Uint8Array([1]),x8.encode(Z.domain??""),new Uint8Array([0]),x8.encode(Z.principal),new Uint8Array([0]),X,J)}function aU($,Z){let Q=iU(Z.protocol);if(!Z.principal)return R5($,x8.encode("\x00anonymous"),Q);let X=x8.encode(Z.principal),J=new Uint8Array(1+X.length);return J[0]=1,J.set(X,1),R5($,J,Q)}function tU($){let Z="";for(let Q=0;Q<$.length;Q+=32768)Z+=String.fromCharCode(...$.subarray(Q,Q+32768));return btoa(Z)}function eU($){let Z=atob($),Q=new Uint8Array(Z.length);for(let X=0;X=1&&J[0]!==u7)throw Error(`Unsupported state token version: ${J[0]}`);let W;try{W=T1(J,Z,{aad:g1(X),version:u7})}catch(M){if(M instanceof nX)throw Error("State token signature verification failed");throw M}if(W.length<8+G8)throw Error("State token truncated");let Y=new DataView(W.buffer,W.byteOffset,W.byteLength),G=0,H=(M,L)=>{let D=new Uint8Array(L);return D.set(W.subarray(M,M+L)),D},z=Number(V5(Y,G));if(G+=8,Q>0){if(Math.floor(Date.now()/1000)-z>Q)throw Error("State token expired")}let q=H(G,G8);G+=G8;let P=l7(Y,G);if(G+=4,G+P>W.length)throw Error("State token truncated (state)");return{stateBytes:H(G,P),callId:q,createdAt:z}}function $M($,Z,Q,X=0){let J=eU($);if(J.length>=1&&J[0]!==p7)throw Error(`Unsupported call token version ${J[0]}`);let W;try{W=T1(J,Z,{aad:sU(Q),version:p7})}catch(w){if(w instanceof nX)throw Error("State token signature verification failed");throw w}if(W.length<8+G8)throw Error("State token truncated");let Y=new DataView(W.buffer,W.byteOffset,W.byteLength),G=(w,T)=>{let E=new Uint8Array(T);return E.set(W.subarray(w,w+T)),E},H=0,z=Number(V5(Y,H));if(H+=8,X>0){if(Math.floor(Date.now()/1000)-z>X)throw Error("State token expired")}let q=G(H,G8);H+=G8;let P=l7(Y,H);if(H+=4,H+P>W.length)throw Error("State token truncated (schema)");let U=G(H,P);H+=P;let M=l7(Y,H);if(H+=4,H+M>W.length)throw Error("State token truncated (input schema)");let L=G(H,M);if(H+=M,H+16!==W.length)throw Error("State token truncated (response budget)");let D=V5(Y,H);H+=8;let R=V5(Y,H);if(D>BigInt(Number.MAX_SAFE_INTEGER)||R>BigInt(Number.MAX_SAFE_INTEGER))throw Error("State token contains an unsafe response budget");let V=Number(D)||void 0,g=Number(R)||void 0;return{callId:q,call:{schemaBytes:U,inputSchemaBytes:L,responseLimitBytes:V,preferredResponseBytes:g}}}var x8,u7=5,p7=2,G8=16,rW="\x00server",HR,zR,qR,PR;var w5=O(()=>{f7();x8=new TextEncoder,HR=x8.encode("vgi_rpc.state.v6\x00"),zR=x8.encode("vgi_rpc.state.v7\x00"),qR=x8.encode("vgi_rpc.call.v3\x00"),PR=x8.encode("vgi_rpc.call.v4\x00")});function n7(){let $=globalThis.process?.env;return Boolean($?.VGI_DISPATCH_DEBUG)}function QM($){let Z=$?.claims?.peer_evidence_binding;return typeof Z==="string"&&Z?Z:void 0}function NR($){return $?.authenticated?$.principal??"":null}function aW($){return{protocol:$.protocolName,principal:NR($.authContext),evidenceBinding:QM($.authContext),domain:$.authContext?.domain}}function XM($,Z){let Q=Z.authContext,X="";for(let J of $)X+=J.toString(16).padStart(2,"0");return`${X}\x00${Z.protocolName}\x00${Q?.authenticated?"1":"0"}\x00${Q?.domain??""}\x00${Q?.principal??""}\x00${QM(Q)??""}`}function JM($){return $.callStateCacheEntries??FR}function i7($,Z,Q){let X=JM(Z);if(X<=0)return;if(v1.size>=X)v1.clear();let J=Z.tokenTtl;v1.set(XM($,Z),{expiresAt:Math.floor(Date.now()/1000)+(J>0?J:3600),call:Q})}function WM(){let $=new Uint8Array(G8);return crypto.getRandomValues($),$}function UR($,Z,Q){let X=XM($,Q),J=JM(Q)>0?v1.get(X):void 0;if(J){if(Math.floor(Date.now()/1000)<=J.expiresAt)return J.call;v1.delete(X)}if(!Z)throw new w$("Missing call token in exchange request",400);let{callId:W,call:Y}=$M(Z,Q.tokenKey,aW(Q),Q.tokenTtl);if(W.length!==$.length||!W.every((G,H)=>G===$[H]))throw new w$("Invalid state token: Malformed state token",400);return i7($,Q,Y),Y}function MR($,Z,Q,X){let J=WM();r7(X,J);let W=aW(X),Y=d7(J,Z,Q,X.tokenKey,W,void 0,{responseLimitBytes:X.maxResponseBytes,preferredResponseBytes:X.preferredResponseBytes});return i7(J,X,{schemaBytes:Z,inputSchemaBytes:Q,responseLimitBytes:X.maxResponseBytes,preferredResponseBytes:X.preferredResponseBytes}),{callId:J,token:E5($,J,X.tokenKey,W),callToken:Y}}function r7($,Z){let Q=$.streamObserver;if(!Q)return;let X="";for(let J of Z)X+=J.toString(16).padStart(2,"0");Q.streamId=X}async function ZM($){return $1($)}function LR($){let Z=$.egress;if(!Z)return;return(Q)=>{Z.externalizedBytes+=Q}}async function a7($,Z){let{schema:Q,batch:X}=await tG($);if(!Z.externalLocation||!F$(X))return{schema:Q,batch:X,dataMetadata:X.metadata??void 0};let J=await E$(X,Z.externalLocation),W=new Map(J.metadata??[]);for(let[Y,G]of X.metadata??[])W.set(Y,G);return{schema:J.schema,batch:e$(J,W),dataMetadata:J.metadata??void 0}}function YM($,Z){try{return yJ($,Z)}catch(Q){let X=Q?.errorMessage,J=new w$(typeof X==="string"?X:String(Q),400);throw J.name=Q instanceof Error?Q.name:"ProtocolError",J}}function AR($,Z){if(!Z?.storage)return 0;if($.numRows===0)return 0;let Q=$.data?.byteLength??0,X=Z.externalizeThresholdBytes??1048576;if(Q0,W=Z.preferredResponseBytes??Z.maxResponseBytes,Y=W!=null&&X>W;if(!J&&!Y)return $;if(Z.maxExternalizedResponseBytes!=null&&X>Z.maxExternalizedResponseBytes)throw DR(`Externalised payload exceeds max_externalized_response_bytes (${X} > ${Z.maxExternalizedResponseBytes}) for method '${Q}'`);return kJ($,Z.externalLocation,LR(Z),Y)}function o7($,Z,Q){let X=q$($,Z,Q.serverId,null),J=fZ(f0($,[X]),500);return J.__dispatchError=Z,J}async function GM($,Z,Q){let X=$.resultSchema,{schema:J,batch:W}=await a7(Z,Q),Y=YM(J,W);if(Y.methodName!==$.name)throw new w$(`Method name in request '${Y.methodName}' does not match URL '${$.name}'`,400);try{k6(J,$.paramsSchema,$.name)}catch(z){let q=z?.errorMessage,P=new w$(typeof q==="string"?q:String(z),400);throw P.name="ProtocolError",P}w1(Y.params,$.defaults);let G=!!Q.externalLocation?.storage,H=new xQ(X,!0,Q.serverId,Y.requestId,Q.authContext,Q.cookies,Q.kind??gQ.HTTP,{remainingResponseBytes:Q.maxResponseBytes,responseLimitBytes:Q.maxResponseBytes,preferredResponseBytes:Q.preferredResponseBytes,remainingExternalizedResponseBytes:G?Q.maxExternalizedResponseBytes:void 0,externalizationEnabled:G,peerEvidence:Q.peerEvidence});if(H.enableCookieSink(),Q.stickyContext)H.attachStickyContext(Q.stickyContext);try{let z=await $.handler(Y.params,H),q=D6(X,z,Q.serverId,Y.requestId);q=await t7(q,Q,$.name);let P=[...H.batches.map((L)=>L.batch),q],U=f0(X,P);if(Q.maxResponseBytes!=null&&U.byteLength>Q.maxResponseBytes){let L=Error(`HTTP body exceeds max_response_bytes (${U.byteLength} > ${Q.maxResponseBytes}) for method '${$.name}'`);L.name="ResponseTooLargeError";let D=o7(X,L,Q);return aG(D.headers,H.drainResponseCookies()),D}let M=fZ(U);return aG(M.headers,H.drainResponseCookies()),M}catch(z){let q=q$(X,z,Q.serverId,Y.requestId),P=fZ(f0(X,[q]),500);return aG(P.headers,H.drainResponseCookies()),P.__dispatchError=z,P}}async function KM($,Z,Q){let X=!!$.producerFn,J=$.outputSchema,W=$.inputSchema??S5,{schema:Y,batch:G}=await a7(Z,Q),H=YM(Y,G);if(H.methodName!==$.name)throw new w$(`Method name in request '${H.methodName}' does not match URL '${$.name}'`,400);try{k6(Y,$.paramsSchema,$.name)}catch(L){let D=L?.errorMessage,R=new w$(typeof D==="string"?D:String(L),400);throw R.name="ProtocolError",R}w1(H.params,$.defaults);let z;try{if(X)z=await $.producerInit(H.params);else z=await $.exchangeInit(H.params)}catch(L){let D=$.headerSchema??S5,R=q$(D,L,Q.serverId,H.requestId),V=fZ(f0(D,[R]),500);return V.__dispatchError=L,V}let q=z?.__outputSchema??J,P=z?.__inputSchema??W,U=z?.__isProducer??X,M=null;if($.headerSchema&&$.headerInit)try{let L=new xQ($.headerSchema,!0,Q.serverId,H.requestId,Q.authContext,Q.cookies,Q.kind??gQ.HTTP,{peerEvidence:Q.peerEvidence}),D=$.headerInit(H.params,z,L),R=D6($.headerSchema,D,Q.serverId,H.requestId),V=[...L.batches.map((g)=>g.batch),R];M=f0($.headerSchema,V)}catch(L){let D=q$($.headerSchema,L,Q.serverId,H.requestId),R=fZ(f0($.headerSchema,[D]),500);return R.__dispatchError=L,R}if(U){let L=WM();r7(Q,L);let D=d7(L,I6(q),I6(P),Q.tokenKey,aW(Q),void 0,{responseLimitBytes:Q.maxResponseBytes,preferredResponseBytes:Q.preferredResponseBytes});return i7(L,Q,{schemaBytes:I6(q),inputSchemaBytes:I6(P),responseLimitBytes:Q.maxResponseBytes,preferredResponseBytes:Q.preferredResponseBytes}),zM($,z,q,P,Q,H.requestId,M,{callId:L,callToken:D},s7(G.metadata))}else{let L=Q.stateSerializer.serialize(z),D=I6(q),R=I6(P),{token:V,callToken:g}=MR(L,D,R,Q),w=new Map;w.set(y$,V),w.set(YX,g);let T=GX(q,w),E=f0(q,[T]),_;if(M)_=qM(M,E);else _=E;return fZ(_)}}function s7($){if(!$)return;let Z=new Map;for(let[Q,X]of $)if(!CR.has(Q))Z.set(Q,X);return Z.size>0?Z:void 0}async function HM($,Z,Q){let X=!!$.producerFn,{batch:J,dataMetadata:W}=await a7(Z,Q),Y=J.metadata?.get(y$);if(!Y)throw new w$("Missing state token in exchange request",400);let G=J.metadata?.get(bX)!=null;if(G&&Q.streamObserver)Q.streamObserver.cancelled=!0;let H;try{H=x1(Y,Q.tokenKey,Q.tokenTtl,aW(Q))}catch(L){throw new w$(`Invalid state token: ${L.message}`,400)}r7(Q,H.callId);let z=UR(H.callId,J.metadata?.get(YX),Q);if(Q={...Q,maxResponseBytes:t$(Q.maxResponseBytes,z.responseLimitBytes),preferredResponseBytes:t$(Q.preferredResponseBytes,z.preferredResponseBytes)},Q.preferredResponseBytes!=null&&Q.maxResponseBytes!=null&&Q.preferredResponseBytes>Q.maxResponseBytes)Q.preferredResponseBytes=Q.maxResponseBytes;let q;try{q=Q.stateSerializer.deserialize(H.stateBytes)}catch(L){throw console.error("[httpDispatchStreamExchange] state deserialize error:",L.message),new w$(`State deserialization failed: ${L.message}`,500)}let P;if(z.schemaBytes.length>0)P=await ZM(z.schemaBytes);else P=q?.__outputSchema??$.outputSchema;let U;if(z.inputSchemaBytes.length>0)U=await ZM(z.inputSchemaBytes);else U=q?.__inputSchema??$.inputSchema??S5;let M=q?.__isProducer??X;if(n7())console.error(`[httpDispatchStreamExchange] method=${$.name} effectiveProducer=${M} stateKeys=${Object.keys(q||{})}`);if(G){if($.onCancel)try{await $.onCancel(q)}catch(L){console.debug?.(`onCancel hook failed: ${L instanceof Error?L.message:L}`)}return fZ(f0(P,[]))}if(M)return zM($,q,P,U,Q,null,null,{callId:H.callId,callToken:null},s7(J.metadata));else{let L=s7(W)??new Map,D=e$(J,L),R=!!Q.externalLocation?.storage,V=new xQ(P,M,Q.serverId,null,Q.authContext,Q.cookies,Q.kind??gQ.HTTP,{remainingResponseBytes:Q.maxResponseBytes,responseLimitBytes:Q.maxResponseBytes,preferredResponseBytes:Q.preferredResponseBytes,remainingExternalizedResponseBytes:R?Q.maxExternalizedResponseBytes:void 0,externalizationEnabled:R,inputMetadata:L,peerEvidence:Q.peerEvidence});if(Q.stickyContext)V.attachStickyContext(Q.stickyContext);let g=D;if(!M&&U!==S5&&D.schema!==U)try{g=U6(D,U)}catch(E){if(E instanceof TypeError)throw E;console.debug?.(`Schema conformance skipped: ${E instanceof Error?E.message:E}`)}try{if($.exchangeFn)await $.exchangeFn(q,g,V);else await $.producerFn(q,V)}catch(E){if(n7())console.error("[httpDispatchStreamExchange] exchange handler error:",E.message,E.stack?.split(` -`).slice(0,5).join(` -`));let _=q$(P,E,Q.serverId,null),y=fZ(f0(P,[_]),500);return y.__dispatchError=E,y}let w=[];if(V.finished)for(let E of V.batches)if(E.metadata&&E.metadata.size>0){let _=new Map(E.batch.metadata??[]);for(let[y,h]of E.metadata)_.set(y,h);w.push(e$(E.batch,_))}else w.push(E.batch);else{let E=Q.stateSerializer.serialize(q),_=E5(E,H.callId,Q.tokenKey,aW(Q));for(let[y,h]of V.batches.entries()){let s=h.batch;if(y===V.dataBatchIdx){let f=new Map(s.metadata??[]);if(h.metadata)for(let[c,o]of h.metadata)f.set(c,o);f.set(y$,_),w.push(e$(s,f))}else w.push(s)}if(!w.some((y)=>y.metadata?.get(y$))){let y=new Map;y.set(y$,_),w.push(GX(P,y))}}try{w=await Promise.all(w.map((E,_)=>_===V.dataBatchIdx?t7(E,Q,$.name):E))}catch(E){return o7(P,E,Q)}let T=f0(P,w);if(Q.maxResponseBytes!=null&&T.byteLength>Q.maxResponseBytes){let E=Error(`HTTP body exceeds max_response_bytes (${T.byteLength} > ${Q.maxResponseBytes}) for method '${$.name}'`);return E.name="ResponseTooLargeError",o7(P,E,Q)}return fZ(T)}}async function zM($,Z,Q,X,J,W,Y,G,H){let z=[],q=J.maxStreamResponseBytes??J.maxResponseBytes,P=J.maxExternalizedResponseBytes,U=!!J.externalLocation?.storage,M,L,D=new xQ(Q,!0,J.serverId,W,J.authContext,J.cookies,J.kind??gQ.HTTP,{remainingResponseBytes:q,responseLimitBytes:q,preferredResponseBytes:J.preferredResponseBytes,remainingExternalizedResponseBytes:U?P:void 0,externalizationEnabled:U,inputMetadata:H,peerEvidence:J.peerEvidence});if(J.stickyContext)D.attachStickyContext(J.stickyContext);try{if($.producerFn)await $.producerFn(Z,D);else{let E=GX(X,H);await $.exchangeFn(Z,E,D)}}catch(E){if(n7())console.error("[produceStreamResponse] error:",E.message,E.stack?.split(` -`).slice(0,3).join(` -`));z.push(q$(Q,E,J.serverId,W)),M=E instanceof Error?E:Error(String(E))}if(!M)for(let E of D.batches){let _=E.batch;if(E.metadata&&E.metadata.size>0){let y=new Map(_.metadata??[]);for(let[h,s]of E.metadata)y.set(h,s);_=e$(_,y)}if(U&&J.externalLocation)try{_=await t7(_,J,$.name)}catch(y){L=y;break}z.push(_)}if(L)z.length=0,z.push(q$(Q,L,J.serverId,W)),M=L;else if(!M&&!D.finished){let E=J.stateSerializer.serialize(Z),_=E5(E,G.callId,J.tokenKey,aW(J)),y=new Map;if(y.set(y$,_),G.callToken)y.set(YX,G.callToken);z.push(GX(Q,y))}let R=f0(Q,z),V;if(q!=null&&R.byteLength+(Y?.byteLength??0)>q)V=OR(`HTTP body exceeds max_response_bytes (${R.byteLength+(Y?.byteLength??0)} > ${q}) for method '${$.name}'`),z.length=0,z.push(q$(Q,V,J.serverId,W)),R=f0(Q,z),Y=null,M=V;let g;if(Y)g=qM(Y,R);else g=R;let T=fZ(g,L||V?500:200);if(M)T.__dispatchError=M;return T}function qM(...$){let Z=$.reduce((J,W)=>J+W.length,0),Q=new Uint8Array(Z),X=0;for(let J of $)Q.set(J,X),X+=J.length;return Q}var FR=4096,v1,S5,CR;var PM=O(()=>{qZ();I8();lX();kU();S1();mX();J8();OJ();w5();v1=new Map;S5=E0([]);CR=new Set([y$,YX,bX])});function l$($){return $.replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function e7($){let Z=$.typeId;if(Z===5)return"str";if(Z===4)return"bytes";if(Z===2)return"int";if(Z===3)return"float";if(Z===6)return"bool";if(Z===12)return"list";if(Z===17)return"map";if(Z===24)return"enum";return $.toString()}function MM($,Z,Q,X){let J=[];if(Q)J.push(`View service API`);if(X)J.push(`Source repository`);return J.push('Learn more about vgi-rpc'),` - - - - -${l$($)} — vgi-rpc -${` - -`} - - - - -

${l$($)}

-

Powered by vgi-rpc (TypeScript) · server ${l$(Z)}

-

This is a vgi-rpc service endpoint.

- - - -`}function LM($,Z){return` - - - - -404 — vgi-rpc -${` - -`} -${``} - - - -

404 — Not Found

-

This is a vgi-rpc service endpoint${Z?` (${l$(Z)})`:""}.

-

RPC methods are available under ${l$($||"/")}/<method>.

- - -`}function jR($){let Z=l$($.name),Q=$.type==="unary",X=!!$.headerSchema,J=[];if(J.push(Q?'unary':'stream'),X)J.push('header');let W="",Y=$.paramsSchema;if(Y.fields.length>0)W=` - -${Y.fields.map((P)=>{let U=l$(P.name),M=l$(e7(P.type)),L=$.defaults&&P.name in $.defaults?l$(JSON.stringify($.defaults[P.name])):"—";return``}).join(` -`)} -
NameTypeDefaultDescription
${U}${M}${L}—
`;else W='

No parameters

';let G="";if(Q&&$.resultSchema.fields.length>0)G=` - -${$.resultSchema.fields.map((P)=>{return``}).join(` -`)} -
NameType
${l$(P.name)}${l$(e7(P.type))}
`;let H="";if(X&&$.headerSchema&&$.headerSchema.fields.length>0)H=` - -${$.headerSchema.fields.map((P)=>{return``}).join(` -`)} -
NameType
${l$(P.name)}${l$(e7(P.type))}
`;let z=$.doc?`

${l$($.doc)}

`:"";return`
-
-${Z} -${J.join(` -`)} -
-${z} -${W} -${G} -${H} -
`}function AM($,Z,Q,X){let W=[...Q.entries()].sort(([G],[H])=>G.localeCompare(H)).map(([,G])=>jR(G)).join(` -`),Y=X?` · Source`:"";return` - - - - -${l$($)} API Reference — vgi-rpc -${` - -`} - - - -
- -

${l$($)}

-

API Reference

-

Powered by vgi-rpc (TypeScript) · server ${l$(Z)}${Y}

-
-${W} - - -`}var FM="https://vgi-rpc-python.query.farm/assets/logo-hero.png",NM=` - -`,UM=``;function B5(){let $=import.meta.require??globalThis.require??null;if(!$)throw Error("OAuth PKCE requires Node.js or Bun (node:crypto).");return $(VR)}function wR(){return jM(32).toString("base64url")}function SR($){return RR("sha256").update($,"ascii").digest().toString("base64url")}function BR(){return jM(24).toString("base64url")}function kR($){return Qz("sha256",$).update("oauth-pkce-session").digest()}function IR($){return $.toString("base64").replace(/\+/g,"-").replace(/\//g,"_")}function _R($){let Z=$.replace(/-/g,"+").replace(/_/g,"/");return Buffer.from(Z,"base64")}function TR($,Z,Q,X,J,W){let Y=J??Math.floor(Date.now()/1000),G=Buffer.from($,"utf-8"),H=Buffer.from(Z,"utf-8"),z=Buffer.from(Q,"utf-8"),q=Buffer.from(W??"","utf-8"),P=11+G.length+2+H.length+2+z.length+2+q.length,U=Buffer.alloc(P),M=0;U.writeUInt8(EM,M),M+=1,U.writeBigUInt64LE(BigInt(Y),M),M+=8,U.writeUInt16LE(G.length,M),M+=2,G.copy(U,M),M+=G.length,U.writeUInt16LE(H.length,M),M+=2,H.copy(U,M),M+=H.length,U.writeUInt16LE(z.length,M),M+=2,z.copy(U,M),M+=z.length,U.writeUInt16LE(q.length,M),M+=2,q.copy(U,M);let L=Qz("sha256",X).update(U).digest();return IR(Buffer.concat([U,L]))}function gR($,Z,Q=wM){let X;try{X=_R($)}catch{throw Error("Malformed session cookie")}if(X.length<49)throw Error("Session cookie too short");let J=X.subarray(0,X.length-CM),W=X.subarray(X.length-CM),Y=Qz("sha256",Z).update(J).digest();if(!VM(W,Y))throw Error("Session cookie signature mismatch");let G=J.readUInt8(0);if(G!==EM)throw Error(`Unexpected session cookie version: ${G}`);let H=Number(J.readBigUInt64LE(1));if(Q>0){let g=Math.floor(Date.now()/1000)-H;if(g<0||g>Q)throw Error(`Session cookie expired (age=${g}s, max=${Q}s)`)}let z=9,q=J.readUInt16LE(z);z+=2;let P=J.subarray(z,z+q).toString("utf-8");z+=q;let U=J.readUInt16LE(z);z+=2;let M=J.subarray(z,z+U).toString("utf-8");z+=U;let L=J.readUInt16LE(z);z+=2;let D=J.subarray(z,z+L).toString("utf-8");z+=L;let R=J.readUInt16LE(z);z+=2;let V=J.subarray(z,z+R).toString("utf-8");return{codeVerifier:P,stateNonce:M,originalUrl:D,returnTo:V}}function xR($){let Z=null;return function(){if(Z)return Z;let X=`${$.replace(/\/+$/,"")}/.well-known/openid-configuration`;return Z=fetch(X,{signal:AbortSignal.timeout(1e4)}).then(async(J)=>{if(!J.ok)throw Error(`OIDC discovery HTTP ${J.status}`);let W=await J.json();return{authorizationEndpoint:W.authorization_endpoint,tokenEndpoint:W.token_endpoint}}).catch(()=>{return Z=null,null}),Z}}async function vR($,Z,Q,X,J,W,Y){let G=new URLSearchParams({grant_type:"authorization_code",code:Z,redirect_uri:Q,code_verifier:X,client_id:J});if(W)G.set("client_secret",W);let H;try{let M=await fetch($,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:G.toString(),signal:AbortSignal.timeout(15000)});if(!M.ok){let L=await M.text();throw Error(`HTTP ${M.status}: ${L}`)}H=await M.json()}catch(M){throw Error(`Token exchange failed: ${M.message??M}`)}let z=H.refresh_token??null,q=H.id_token??null;if(Y){let M=H.id_token;if(!M)throw Error("Token response missing id_token");try{let L=M.split(".");if(L.length>=2){let D=4-L[1].length%4,R=Buffer.from(L[1]+"=".repeat(D%4),"base64").toString("utf-8"),V=JSON.parse(R);if(V.exp!=null){let g=Math.max(Number(V.exp)-Math.floor(Date.now()/1000),60);return{token:M,maxAge:g,refreshToken:z,idToken:q}}}}catch{}return{token:M,maxAge:DM,refreshToken:z,idToken:q}}let P=H.access_token;if(!P)throw Error("Token response missing access_token");let U=H.expires_in??DM;return{token:P,maxAge:Number(U),refreshToken:z,idToken:q}}function BM($,Z){let Q=$;if(Q.length>OM)Q=Q.slice(0,OM);try{let X=new URL(Q,"http://dummy");if(Q.startsWith("http://")||Q.startsWith("https://")||Q.startsWith("//"))return Z||"/";if(X.hostname!=="dummy")return Z||"/"}catch{return Z||"/"}if(Z&&!Q.startsWith(Z))return Z||"/";return Q}function yR($){return $==="localhost"||$==="127.0.0.1"||$==="[::1]"}function kM($,Z){let Q=Z??SM;if(!$||$.length>2048)return"";let X;try{X=new URL($)}catch{return""}if(X.protocol!=="http:"&&X.protocol!=="https:")return"";if(!X.hostname)return"";if(yR(X.hostname)&&X.protocol==="http:")return $;let J=`${X.protocol}//${X.hostname}`;if(Q.has(J))return $;if(X.port){let W=`${X.protocol}//${X.hostname}:${X.port}`;if(Q.has(W))return $}return""}function Xz($){let Z=$.headers.get("Cookie"),Q=new Map;if(!Z)return Q;for(let X of Z.split(";")){let J=X.indexOf("=");if(J<0)continue;let W=X.slice(0,J).trim(),Y=X.slice(J+1).trim();Q.set(W,Y)}return Q}function hR($){if(!$)return null;try{let Z=$.split(".");if(Z.length<2)return null;let Q=(4-Z[1].length%4)%4,X=Buffer.from(Z[1]+"=".repeat(Q),"base64").toString("utf-8"),J=JSON.parse(X);return J&&typeof J==="object"?J:null}catch{return null}}function bR($){let Z=hR($);if(!Z)return null;let Q={};for(let X of ER)if(Z[X]!=null)Q[X]=Z[X];if(Object.keys(Q).length===0)return null;return Buffer.from(JSON.stringify(Q),"utf-8").toString("base64url")}function tW($,Z,Q){let X=`${$}=${Z}`;if(Q.maxAge!==void 0)X+=`; Max-Age=${Q.maxAge}`;if(Q.path)X+=`; Path=${Q.path}`;if(Q.secure)X+="; Secure";if(Q.httpOnly)X+="; HttpOnly";if(Q.sameSite)X+=`; SameSite=${Q.sameSite}`;return X}function $z($){return $.replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function mR($,Z,Q){let X=Z?`
${$z(Z)}
`:"";return` - - - - -Authentication Error — vgi-rpc -${NM} -${UM} - - - -

Authentication Error

-

${$z($)}

-${X} -

Try again

- - -`}function fR($,Z=k5){return async function(X){let W=Xz(X).get(Z);if(!W)throw Error("No auth cookie");let Y=new Headers(X.headers);Y.set("Authorization",`Bearer ${W}`);let G=new Request(X.url,{method:X.method,headers:Y});return $(G)}}function IM($,Z){if($&&$.length>0)return $.join(" ");return Z}function _M($,Z){let Q=kR($.signingKey),X=xR($.issuer);return{sessionKey:Q,oidcDiscovery:X,clientId:$.clientId,clientSecret:$.clientSecret,useIdToken:$.useIdToken??!1,prefix:$.prefix,secureCookie:$.secureCookie,redirectUri:$.redirectUri,scope:$.scope??"openid email",allowedReturnOrigins:$.allowedReturnOrigins??SM,cookieAuthenticate:fR(Z)}}function pR($){return $==="localhost"||$==="127.0.0.1"||$==="[::1]"}function cR($,Z,Q){$.append("Vary","Origin");let X=Z.headers.get("Origin");if(!X)return;let J;try{J=new URL(X)}catch{return}if(J.protocol!=="http:"&&J.protocol!=="https:")return;if(!J.hostname)return;if(pR(J.hostname)&&J.protocol==="http:"){$.set("Access-Control-Allow-Origin",X);return}if(Q.allowedReturnOrigins.has(X))$.set("Access-Control-Allow-Origin",X)}function g6($,Z,Q,X){return $.set("Content-Type","application/json"),new Response(JSON.stringify({error:Q,error_description:X}),{status:Z,headers:$})}async function TM($,Z){let Q=new Headers;if(cR(Q,$,Z),$.method==="OPTIONS")return Q.set("Access-Control-Allow-Methods","POST, OPTIONS"),Q.set("Access-Control-Allow-Headers","Content-Type"),Q.set("Access-Control-Max-Age","300"),new Response(null,{status:204,headers:Q});if($.method!=="POST")return new Response(null,{status:405,headers:Q});if(($.headers.get("Content-Type")??"").split(";")[0].trim().toLowerCase()!=="application/x-www-form-urlencoded")return g6(Q,415,"invalid_request","Content-Type must be application/x-www-form-urlencoded");let J;try{J=await $.text()}catch{return g6(Q,400,"invalid_request","Could not read request body")}let W;try{W=new URLSearchParams(J)}catch{return g6(Q,400,"invalid_request","Could not parse form body")}let Y=W.get("grant_type")??"";if(!uR.has(Y))return g6(Q,400,"unsupported_grant_type","grant_type must be authorization_code or refresh_token");let G=W.get("client_id");if(G&&G!==Z.clientId)return g6(Q,400,"invalid_client","client_id does not match the configured client");let H=await Z.oidcDiscovery();if(!H)return g6(Q,502,"server_error","Authorization server discovery failed");let z=new URLSearchParams;if(z.set("grant_type",Y),z.set("client_id",Z.clientId),Z.clientSecret)z.set("client_secret",Z.clientSecret);for(let M of["code","code_verifier","redirect_uri","refresh_token","scope"]){let L=W.get(M);if(L!==null&&L!=="")z.set(M,L)}let q;try{q=await fetch(H.tokenEndpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:z.toString(),signal:AbortSignal.timeout(15000)})}catch(M){return g6(Q,502,"server_error",`Upstream token endpoint failed: ${M?.message??M}`)}let P=new Uint8Array(await q.arrayBuffer()),U=q.headers.get("content-type")??"application/json";return Q.set("Content-Type",U),new Response(P,{status:q.status,headers:Q})}async function gM($,Z){let Q=new URL($.url),X=Z.prefix||"/";function J(E,_,y){return new Response(mR(_,y,X),{status:E,headers:{"Content-Type":"text/html; charset=utf-8"}})}let W=Q.searchParams.get("error");if(W){let E=Q.searchParams.get("error_description")??W;return J(400,"The authorization server returned an error.",E)}let Y=Q.searchParams.get("code"),G=Q.searchParams.get("state");if(!Y||!G)return J(400,"Missing authorization code or state parameter.",null);let z=Xz($).get(Zz);if(!z)return J(400,"Session cookie missing or expired. Please try again.",null);let q;try{q=gR(z,Z.sessionKey)}catch{return J(400,"Session expired or invalid. Please try again.",null)}let P=Buffer.from(G,"utf-8"),U=Buffer.from(q.stateNonce,"utf-8");if(P.length!==U.length||!VM(P,U))return J(400,"State mismatch — possible CSRF. Please try again.",null);let M=await Z.oidcDiscovery();if(!M)return J(502,"Could not reach the authorization server.","OIDC discovery failed.");let L;try{L=await vR(M.tokenEndpoint,Y,Z.redirectUri,q.codeVerifier,Z.clientId,Z.clientSecret,Z.useIdToken)}catch(E){return J(502,"Token exchange with the authorization server failed.",String(E.message??E))}let D=tW(Zz,"",{maxAge:0,path:`${Z.prefix}/_oauth/`,secure:Z.secureCookie,httpOnly:!0,sameSite:"Lax"});if(q.returnTo){let E=q.returnTo.includes("#")?"&":"#",_=[`token=${L.token}`];if(L.refreshToken)_.push(`refresh_token=${encodeURIComponent(L.refreshToken)}`);if(_.push(`token_endpoint=${encodeURIComponent(M.tokenEndpoint)}`),_.push(`client_id=${encodeURIComponent(Z.clientId)}`),Z.clientSecret)_.push(`client_secret=${encodeURIComponent(Z.clientSecret)}`);if(Z.useIdToken)_.push("use_id_token=true");let y=`${q.returnTo}${E}${_.join("&")}`;return new Response(null,{status:302,headers:{Location:y,"Cache-Control":"no-cache, no-store, must-revalidate","Content-Type":"text/html; charset=utf-8","Set-Cookie":D}})}let R=BM(q.originalUrl,Z.prefix),V=Z.prefix||"/",g=tW(k5,L.token,{maxAge:L.maxAge,path:V,secure:Z.secureCookie,httpOnly:!1,sameSite:"Lax"}),w=new Headers;w.set("Location",R),w.set("Cache-Control","no-cache, no-store, must-revalidate"),w.set("Content-Type","text/html; charset=utf-8"),w.append("Set-Cookie",g),w.append("Set-Cookie",D);let T=bR(L.idToken??(Z.useIdToken?L.token:null));if(T!==null)w.append("Set-Cookie",tW(RM,T,{maxAge:L.maxAge,path:V,secure:Z.secureCookie,httpOnly:!1,sameSite:"Lax"}));return new Response(null,{status:302,headers:w})}function xM($,Z){let Q=Z.prefix||"/",X=tW(k5,"",{maxAge:0,path:Q,secure:Z.secureCookie,httpOnly:!1}),J=tW(RM,"",{maxAge:0,path:Q,secure:Z.secureCookie,httpOnly:!1}),W=new Headers;return W.set("Location",Z.prefix||"/"),W.append("Set-Cookie",X),W.append("Set-Cookie",J),new Response(null,{status:302,headers:W})}async function vM($,Z){if(!($.headers.get("Accept")??"").includes("text/html"))return null;let X=await Z.oidcDiscovery();if(!X)return null;let J=new URL($.url),W=wR(),Y=SR(W),G=BR(),H=J.pathname;if(J.search)H=`${H}${J.search}`;H=BM(H,Z.prefix);let z=kM(J.searchParams.get("_vgi_return_to")??"",Z.allowedReturnOrigins),q=TR(W,G,H,Z.sessionKey,void 0,z),P=new URLSearchParams({response_type:"code",client_id:Z.clientId,redirect_uri:Z.redirectUri,code_challenge:Y,code_challenge_method:"S256",state:G,scope:Z.scope});if(z)P.set("access_type","offline"),P.set("prompt","consent");let U=`${X.authorizationEndpoint}?${P.toString()}`,M=tW(Zz,q,{maxAge:wM,path:`${Z.prefix}/_oauth/`,secure:Z.secureCookie,httpOnly:!0,sameSite:"Lax"});return new Response(null,{status:302,headers:{Location:U,"Cache-Control":"no-cache, no-store, must-revalidate","Content-Type":"text/html; charset=utf-8","Set-Cookie":M}})}function yM($,Z){let Q=new URL($.url),X=kM(Q.searchParams.get("_vgi_return_to")??"",Z.allowedReturnOrigins);if(!X)return null;let W=Xz($).get(k5);if(!W)return null;try{let z=W.split(".");if(z.length>=2){let q=JSON.parse(Buffer.from(z[1],"base64url").toString("utf-8"));if(typeof q.exp==="number"&&q.exp<=Math.floor(Date.now()/1000))return null}}catch{}let Y=X.includes("#")?"&":"#",G=[`token=${W}`],H=`${X}${Y}${G.join("&")}`;return new Response(null,{status:302,headers:{Location:H,"Cache-Control":"no-cache, no-store, must-revalidate"}})}var VR="node:crypto",RR=($)=>B5().createHash($),Qz=($,Z)=>B5().createHmac($,Z),jM=($)=>B5().randomBytes($),VM=($,Z)=>B5().timingSafeEqual($,Z),Zz="_vgi_oauth_session",k5="_vgi_auth",RM="_vgi_identity",ER,EM=4,wM=600,DM=3600,OM=2048,CM=32,SM,uR;var hM=O(()=>{ER=["sub","email","preferred_username","name","picture"],SM=new Set(["https://cupola.query-farm.services"]);uR=new Set(["authorization_code","refresh_token"])});var bM="VGI-Proxy-Proof",Jz="VGI-Proxy-Proof-Required";var mM=()=>{};function nR($){let Z="";for(let Q=0;Q<$.length;Q+=32768)Z+=String.fromCharCode(...$.subarray(Q,Q+32768));return btoa(Z).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function oR($){let Z=$.replace(/-/g,"+").replace(/_/g,"/"),Q=Z.length%4;if(Q===2)Z+="==";else if(Q===3)Z+="=";else if(Q===1)throw Error("invalid base64url length");let X=atob(Z),J=new Uint8Array(X.length);for(let W=0;W255)throw Error(`server_id too long (${Y.length} bytes); max 255`);let G=new Uint8Array(_5+Y.length+oX+pM),H=new DataView(G.buffer),z=0;H.setBigUint64(z,BigInt(W??Math.floor(Date.now()/1000)),!0),z+=8,G[z]=Y.length,z+=1,G.set(Y,z),z+=Y.length,G.set(Z,z),z+=oX,H.setBigUint64(z,BigInt(Q),!0);let q=_1(G,X,{aad:J,version:uM});return nR(q)}function Wz($,Z,Q){let X;try{X=oR($)}catch{throw new B8("malformed session token")}let J;try{J=T1(X,Z,{aad:Q,version:uM})}catch(U){if(U instanceof nX)throw new B8("session token verification failed");throw U}if(J.length<_5)throw new B8("malformed session token");let W=new DataView(J.buffer,J.byteOffset,J.byteLength),Y=J[8],G=_5+Y,H=G+oX+pM;if(J.length!==H)throw new B8("malformed session token");let z=dR.decode(J.subarray(_5,G)),q=new Uint8Array(oX);q.set(J.subarray(G,G+oX));let P=Number(W.getBigUint64(G+oX,!0));return{serverId:z,sessionId:q,expiresAt:P}}class lM{locked=!1;waiters=[];async acquire(){if(!this.locked)return this.locked=!0,()=>this.release();return await new Promise(($)=>this.waiters.push($)),this.locked=!0,()=>this.release()}release(){let $=this.waiters.shift();if($)$();else this.locked=!1}}function T5($,Z,Q,X){let J=$?`${Z??""}\x00${Q??""}`:"\x00anonymous";return X?`${J}\x00${X}`:J}function eW($){let Z="";for(let Q=0;Q<$.length;Q++)Z+=$[Q].toString(16).padStart(2,"0");return Z}function fM($,Z){if($.length!==Z.length)return!1;for(let Q=0;Q<$.length;Q++)if($[Q]!==Z[Q])return!1;return!0}class Yz{defaultTtl;entries=new Map;_draining=!1;constructor($){this.defaultTtl=$}get draining(){return this._draining}setDraining($){this._draining=$}open($,Z,Q){if(this._draining)throw new sY("server is draining — new sessions are rejected");let X=Z??this.defaultTtl,J=Math.floor(Date.now()/1000)+X,W=hW(oX),Y=eW(W);return this.entries.set(Y,{id:W,entry:{state:$,expiresAt:J,principalKey:Q,lock:new lM}}),{sessionId:W,expiresAt:J}}get($,Z){let Q=eW($),X=this.entries.get(Q);if(!X)return null;if(!fM(X.id,$))return null;let J=Math.floor(Date.now()/1000);if(X.entry.expiresAt{try{$.drainExpired()}catch{}},Z);return Q.unref?.(),()=>clearInterval(Q)}function nM($,Z){return{drain:()=>$.setDraining(!0),shutdown:()=>{Z?.(),$.shutdown()},isDraining:()=>$.draining,setDraining:(Q)=>$.setDraining(Q)}}var lR,dR,uM=1,oX=12,_5=9,pM=8;var oM=O(()=>{f7();HZ();R6();lR=new TextEncoder,dR=new TextDecoder("utf-8",{fatal:!1})});var y1;var Gz=O(()=>{y1={serialize($){return new TextEncoder().encode(JSON.stringify($,(Z,Q)=>typeof Q==="bigint"?`__bigint__:${Q}`:Q))},deserialize($){return JSON.parse(new TextDecoder().decode($),(Z,Q)=>typeof Q==="string"&&Q.startsWith("__bigint__:")?BigInt(Q.slice(11)):Q)}}});async function iR($,Z){if(Z==null)return new Uint8Array(await $.arrayBuffer());let Q=$.headers.get("Content-Length");if(Q!=null){let H=Number(Q);if(Number.isFinite(H)&&H>Z)throw new w$("Request body too large",413)}if(!$.body)return new Uint8Array;let X=$.body.getReader(),J=[],W=0;try{while(!0){let{done:H,value:z}=await X.read();if(H)break;if(W+=z.byteLength,W>Z)throw await X.cancel("request body limit exceeded"),new w$("Request body too large",413);J.push(z)}}finally{X.releaseLock()}let Y=new Uint8Array(W),G=0;for(let H of J)Y.set(H,G),G+=H.byteLength;return Y}function rR($){let Z=$.headers.get("cookie");if(!Z)return sR;let Q=new Map;for(let X of Z.split(";")){let J=X.trim();if(!J)continue;let W=J.indexOf("=");if(W<0)continue;Q.set(J.slice(0,W).trim(),J.slice(W+1).trim())}return Q}function Kz($,Z){let Q=Q7($)?$.bindings():new Map([[$.name,{name:$.name,protocol:$,versionExempt:!1}]]);if(!Q7($))Q.set(uZ,{name:uZ,protocol:z5({listBindings:()=>Q,hashFor:(B)=>{let v=Q.get(B);return v?cW(v):Promise.resolve("")},serverId:()=>V,serverVersion:()=>""}),versionExempt:!0});let X=[...Q.keys()].sort();for(let B of X)try{xW(B,!0)}catch(v){throw Error(`Cannot serve protocol '${B}' over HTTP: ${v.message}`)}let J=[...Q.values()][0];if(!J)throw Error("createHttpHandler was given a server that hosts no protocols.");let W=J.protocol,Y=(Z?.prefix??"").replace(/\/+$/,""),G=Z?.tokenKey??hW(32),H=Z?.tokenTtl??3600,z=Z?.corsOrigins,q=Z?.corsMaxAge===void 0?300:Z.corsMaxAge,P=t$(bG(Z?.maxRequestBytes,"maxRequestBytes"),bG(Z?.hostingMaxRequestBytes,"hostingMaxRequestBytes")),U=Z?.maxDecompressedRequestBytes,M=P==null?U:U==null?P:Math.min(P,U),L=t$(WQ(Z?.maxResponseBytes??Z?.maxStreamResponseBytes,"maxResponseBytes"),WQ(Z?.hostingMaxResponseBytes,"hostingMaxResponseBytes")),D=WQ(Z?.preferredResponseBytes,"preferredResponseBytes"),R=Z?.maxExternalizedResponseBytes,V=Z?.serverId??crypto.randomUUID().replace(/-/g,"").slice(0,12),g=Z?.authenticate,w=Z?.oauthResourceMetadata,T=[...Z?.peerIdentityProviders??[]],E=Z?.peerAuthenticationPolicy,_=Z?.peerResolutionTimeoutMs??5000;if(!Number.isFinite(_)||_<=0)throw TypeError("peerResolutionTimeoutMs must be positive");let y=Z?.peerProviderConcurrency??64;if(!Number.isInteger(y)||y<=0)throw TypeError("peerProviderConcurrency must be a positive integer");let h=0,s=new Set;for(let B of T){if(!B?.provider||s.has(B.provider))throw TypeError("peer identity providers must have unique non-empty names");s.add(B.provider)}if(y - Server: ${w0} - Direction: the client did not send a vgi_rpc.protocol_version metadata key. This is either a vgi-rpc framework bug or a non-VGI client connecting to a VGI worker.`);let t0;try{t0=VW(L0)}catch{throw new xX(`VGI client/worker protocol_version mismatch. - Client: ${L0} - Server: ${w0} - Direction: client sent a malformed protocol_version. Expected canonical semver MAJOR.MINOR.PATCH.`)}if(t0[0]===i[0]&&t0[1]===i[1])return;let oZ=t0[0]x0(B));n0=v;try{await v,d0=!0}finally{if(n0===v)n0=null}}let k0=Z?.enableLandingPage??!0,X0=Z?.enableDescribePage??!0,h0=Z?.enableNotFoundPage??!0,I0=Z?.protocolName??W.name,v0=Z?.repositoryUrl??null,N$=Z?.extraRoutes??null,lZ=f!=null,B$=k0?MM(I0,V,X0?`${Y}/describe`:null,v0):null,dZ=X0?AM(I0,V,c,v0):null,H8=h0?LM(Y,I0):null,PQ=Z?.externalLocation,f8=Z?.uploadUrlProvider,zY=Z?.maxUploadBytes,u8=Z?.proxyProofRequired===!0,f6=[...u8?[bM]:[],...Z?.proxyAuthHeaders??[]],nZ=T7(f6),k$=Z?.enableSticky===!0,I$=Z?.stickyDefaultTtl??300,LX=k$?Object.entries(Z?.stickyEchoHeaders??{}):[],mQ=k$?new Yz(I$):null,nA=mQ?dM(mQ):null;if(Z?._onStickyHandle&&mQ)Z._onStickyHandle(nM(mQ,nA??void 0));let oA=u!=null&&sH();function q9(B){if(u==null)return!1;return B==="zstd"?oA:!0}function sA(B){return!0}let iA=rH.filter((B)=>sA(B)&&q9(B));function kZ(B,v=!1){if(B.set(aH,iA.join(", ")),P!=null)B.set("VGI-Max-Request-Bytes",String(P));if(L!=null)B.set("VGI-Max-Response-Bytes",String(L));if(B.set(hG,"true"),R!=null)B.set("VGI-Max-Externalized-Response-Bytes",String(R));if(B.set("VGI-Externalization-Enabled",PQ?.storage?"true":"false"),f8){if(B.set("VGI-Upload-URL-Support","true"),zY!=null)B.set("VGI-Max-Upload-Bytes",String(zY))}if(u8)B.set(Jz,"true");if(k$){if(B.set(eH,"true"),B.set($7,String(Math.floor(I$))),LX.length>0)B.set(Z7,LX.map(([i])=>i).join(", "))}if(v){if(!B.has("Cache-Control"))B.set("Cache-Control","public, max-age=300")}}let rA={tokenKey:G,tokenTtl:H,serverId:V,protocolName:J.name,maxResponseBytes:L,preferredResponseBytes:D,maxExternalizedResponseBytes:R,stateSerializer:B0,externalLocation:PQ,kind:S,callStateCacheEntries:Z?.callStateCacheEntries},aA=["WWW-Authenticate",lG,IJ,BJ,"VGI-Max-Response-Bytes","VGI-Max-Externalized-Response-Bytes","VGI-Externalization-Enabled",hG,aH,...P!=null?["VGI-Max-Request-Bytes"]:[],...f8?["VGI-Upload-URL-Support",...zY!=null?["VGI-Max-Upload-Bytes"]:[]]:[],...u8?[Jz]:[],...k$?[eH,$7,...LX.length>0?[Z7]:[],fX,_W,...LX.map(([B])=>`${TW}${B}`)]:[],nW,...nZ?[oW]:[]].join(", ");function FZ(B,v=!1,i){if(z){if(B.set("Access-Control-Allow-Origin",z),B.set("Access-Control-Allow-Methods","POST, OPTIONS"),B.set("Access-Control-Allow-Headers",i&&i.length>0?i:`Content-Type, Authorization, ${bZ}`),B.set("Access-Control-Expose-Headers",aA),B.set("Cross-Origin-Resource-Policy","cross-origin"),v&&q!=null)B.set("Access-Control-Max-Age",String(q))}}function tA(B){return B.includes("//")?B.replace(/\/{2,}/g,"/"):B}function eA(B){if(!B.startsWith(`${Y}/`))return null;let v=B.slice(Y.length+1),i="call";if(v.endsWith("/init"))i="init",v=v.slice(0,-5);else if(v.endsWith("/exchange"))i="exchange",v=v.slice(0,-9);let w0=v.indexOf("/");if(w0<=0)return null;let L0=v.slice(0,w0),t0=v.slice(w0+1);if(!t0||t0.includes("/"))return null;return{protocolName:L0,methodName:t0,action:i}}function $D(B,v){if(B.includes("%"))return vW.percentEncoded();try{xW(B,!0)}catch(L0){return new T8(`'vgi_rpc.protocol' is not a protocol name: ${L0.message}`)}let i=Q.get(B);if(!i)return T8.notHosted(B,X);if(v===pW)return new XX(U1());let w0=i.protocol.getMethod(v);if(!w0){let L0=i.protocol.methodNames();return new XX(`Protocol '${B}' has no method '${v}'. Available: [${L0.join(", ")}].`)}return{binding:i,method:w0}}function ZD(B){return jN(F0?null:B.headers.get("Accept-Encoding"),B.headers.get(_J),q9)}async function z8(B,v,i){let{codec:w0,usedCustom:L0}=v,t0=new Uint8Array(await B.arrayBuffer());if(i!=null&&t0.byteLength>i){let FQ=new Headers(B.headers);FQ.delete(C6),FQ.delete(IJ);let p8=Error(`HTTP body exceeds max_response_bytes (${t0.byteLength} > ${i})`);p8.name="ResponseTooLargeError";let m$=q$(g5,p8,V,null),DX=f0(g5,[m$]);return FQ.set("Content-Type",OZ),FQ.set(BJ,"true"),FQ.set("Content-Length",String(DX.byteLength)),new Response(DX,{status:200,headers:FQ})}if(u==null||!w0)return new Response(t0,{status:B.status,headers:B.headers});let o0=w0==="zstd"?await W1(t0,u):await RN(t0),oZ=new Headers(B.headers),n$=L0||F0;return oZ.set(n$?IJ:C6,w0),new Response(o0,{status:B.status,headers:oZ})}function QD(B,v,i){if(i.set("Content-Type","application/json"),i.set(nW,B),nZ)i.set(oW,"true");return i.set("Cache-Control","no-store"),new Response(g7(B,v,nZ),{status:401,headers:i})}function XD(B,v){if(B instanceof qX||B instanceof BZ){let L0=new Headers({"Content-Type":"application/json","Cache-Control":"no-store"});FZ(L0);let t0=Number.isFinite(B.retryAfter)&&B.retryAfter>=0?Math.ceil(B.retryAfter):5;L0.set("Retry-After",String(t0));let o0=B instanceof BZ?"peer identity unavailable":"authentication authority unavailable";return new Response(JSON.stringify({error:"authentication_unavailable",detail:o0}),{status:503,headers:L0})}let i=new Headers;if(FZ(i),w){let L0=new URL(v.url);L0.pathname=I7(Y),L0.search="",i.set("WWW-Authenticate",wU(L0.toString(),w.clientId,w.clientSecret,w.useIdTokenAsBearer,w.deviceCodeClientId,w.deviceCodeClientSecret))}let{reason:w0}=_7(B);return QD(w0,"authentication rejected",i)}function P9(B){let v=B?.claims?.peer_evidence_binding;return typeof v==="string"&&v?v:void 0}async function F9(B){let v=zZ.anonymous(),i;if(g)try{v=await g(B)??zZ.anonymous()}catch(L0){if(E&&L0 instanceof qQ&&L0.reason===CZ.MissingCredential)i=L0;else throw L0}let w0=cX.EMPTY;if(T.length>0){let L0=new AbortController,t0=Date.now()+_,o0=performance.now(),oZ=setTimeout(()=>L0.abort(),_),n$=new Promise((FQ,p8)=>{let m$=()=>p8(new BZ("peer identity resolution timed out"));if(L0.signal.aborted)m$();else L0.signal.addEventListener("abort",m$,{once:!0})});try{let FQ;try{FQ=await Promise.race([Promise.resolve(Z?.peerResolutionContext?.(B)),n$])??{}}catch(X$){if(X$ instanceof A$||X$ instanceof BZ)throw X$;throw new BZ("peer identity resolution context failed")}let p8=_-(performance.now()-o0);if(p8<=0||L0.signal.aborted)throw new BZ("peer identity resolution timed out");let m$=new P1("http",{authority:new URL(B.url).host,serviceName:Z?.peerServiceName,headers:new Map,...FQ,deadline:t0,budgetMs:p8}),DX=Array(T.length),u6=T.map((X$,IZ)=>{if(h>=y)return DX[IZ]=new P$(X$.provider,C0.UNAVAILABLE),Promise.resolve();return h++,Promise.resolve().then(()=>X$.resolve(m$,L0.signal)).then((aX)=>{DX[IZ]=aX&&aX.provider===X$.provider?aX:new P$(X$.provider,C0.INVALID)}).catch((aX)=>{DX[IZ]=new P$(X$.provider,aX instanceof A$?C0.INVALID:C0.UNAVAILABLE)}).finally(()=>{h--})});await Promise.race([Promise.all(u6),n$]).catch((X$)=>{if(!(X$ instanceof BZ))throw X$}),await Promise.resolve();let NQ=Array.from({length:T.length},(X$,IZ)=>DX[IZ]??new P$(T[IZ].provider,C0.UNAVAILABLE));w0=new cX(NQ)}finally{clearTimeout(oZ)}}if(E)try{v=await E(w0,v)}catch(L0){if(L0 instanceof BZ)throw new BZ;if(L0 instanceof A$)throw new A$("peer identity authentication rejected",L0.vgiAuthReason);throw new A$("peer identity authentication rejected")}if(i&&!v.authenticated)throw i;return{authContext:v,peerEvidence:w0}}function AX(B,v,i=g5){let w0=q$(i,B,V,null),L0=f0(i,[w0]),t0=fZ(L0,v);return FZ(t0.headers),t0}function N9(B,v,i=!1){let w0=Error(`Invalid ${bZ}: ${B instanceof Error?B.message:String(B)}`);w0.name="ValueError";let L0=AX(w0,400);if(L0.headers.set(BJ,"true"),i)FZ(L0.headers,!0,v.headers.get("Access-Control-Request-Headers"));return kZ(L0.headers,i),L0}let JD=Z?.enableHealthEndpoint??!0,WD=`${Y}/health`,e5=JD?JSON.stringify({status:"ok",server_id:V,protocol:I0}):null,U9=async function(v,i,w0,L0=null){let t0=new URL(v.url),o0=tA(t0.pathname);if(f&&o0===`${Y}/_oauth/token`&&(v.method==="POST"||v.method==="OPTIONS"))return TM(v,f);if(e5!==null&&(v.method==="GET"||v.method==="HEAD")&&o0===WD){let x=new Headers({"Content-Type":"application/json"});if(FZ(x),kZ(x),v.method==="HEAD")return x.set("Content-Length",String(new TextEncoder().encode(e5).byteLength)),new Response(null,{status:200,headers:x});return new Response(e5,{status:200,headers:x})}if(w&&o0===I7(Y)){if(v.method!=="GET")return new Response("Method Not Allowed",{status:405});let x=V1(w);if(f&&w.clientSecret){let XZ=new URL(w.resource);x.token_endpoint=`${XZ.protocol}//${XZ.host}${Y}/_oauth/token`}let b0=JSON.stringify(x),U$=new Headers({"Content-Type":"application/json","Cache-Control":"public, max-age=60"});return FZ(U$),new Response(b0,{status:200,headers:U$})}if(v.method==="OPTIONS"){let x=v.headers.get(bZ);if(x!==null)try{iY(x)}catch(U$){return N9(U$,v,!0)}let b0=new Headers;return FZ(b0,!0,v.headers.get("Access-Control-Request-Headers")),kZ(b0,!0),new Response(null,{status:204,headers:b0})}if(v.method==="GET"){if(f){if(o0===`${Y}/_oauth/callback`)return gM(v,f);if(o0===`${Y}/_oauth/logout`)return xM(v,f);let x=yM(v,f);if(x)return x}if(g&&f)try{await g(v)}catch{let x=await vM(v,f);if(x)return x}if(N$){let x=t0.pathname===o0?t0:new URL(t0.href);if(x!==t0)x.pathname=o0;let b0=await N$(v,{url:x,prefix:Y,serverId:V,oauthActive:lZ,addCorsHeaders:FZ});if(b0)return b0}if(B$&&(o0===Y||o0===`${Y}/`)){let x=new Headers({"Content-Type":"text/html; charset=utf-8"});return FZ(x),new Response(B$,{status:200,headers:x})}if(dZ&&o0===`${Y}/describe`){let x=new Headers({"Content-Type":"text/html; charset=utf-8"});return FZ(x),new Response(dZ,{status:200,headers:x})}if(H8){let x=new Headers({"Content-Type":"text/html; charset=utf-8"});return FZ(x),new Response(H8,{status:404,headers:x})}return new Response("Not Found",{status:404})}if(v.method==="DELETE"&&k$&&mQ&&o0===`${Y}/${rG}`){let x=new Headers;FZ(x),kZ(x);let b0=(v.headers.get(fX)??"").trim();if(!b0)return new Response(null,{status:200,headers:x});let U$=T5(!1,null,null),XZ=null,VZ=null,l8;try{let f$=(await F9(v)).authContext;if(l8=P9(f$),f$.authenticated)XZ=f$.principal??"",VZ=f$.domain;U$=T5(f$.authenticated,f$.domain,f$.principal,l8)}catch{}let dJ=g1({protocol:rW,principal:XZ,evidenceBinding:l8,domain:VZ}),CX;try{CX=Wz(b0,G,dJ)}catch{return new Response(null,{status:200,headers:x})}if(CX.serverId!==V)return new Response(null,{status:200,headers:x});let nJ=mQ.get(CX.sessionId,U$);if(!nJ)return new Response(null,{status:200,headers:x});let _Z=await nJ.lock.acquire();try{mQ.close(CX.sessionId)}finally{_Z()}return x.set(_W,"true"),new Response(null,{status:204,headers:x})}if(v.method!=="POST")return new Response("Method Not Allowed",{status:405});let oZ={},n$;try{n$=await F9(v)}catch(x){return XD(x,v)}let FQ,p8=v.headers.get(bZ);if(p8!==null)try{FQ=iY(p8)}catch(x){return N9(x,v)}let m$=t$(L,FQ),DX=D==null?void 0:t$(D,m$),u6={...rA,maxResponseBytes:m$,preferredResponseBytes:DX,authContext:n$.authContext,peerEvidence:n$.peerEvidence,cookies:rR(v),egress:w0,streamObserver:oZ},NQ=ZD(v),X$=null,IZ=null;if(k$&&mQ){let x=u6.authContext,b0=x?.authenticated?x.principal??"":null,U$=P9(x),XZ=T5(!!x?.authenticated,x?.domain,x?.principal,U$),VZ=g1({protocol:rW,principal:b0,evidenceBinding:U$,domain:x?.domain}),l8=(v.headers.get(iG)??"").trim().toLowerCase()==="true",dJ=(v.headers.get(fX)??"").trim(),CX=null,nJ=null;if(dJ){let C$;try{if(C$=Wz(dJ,G,VZ),C$.serverId!==V)throw new B8("session token was issued by a different worker (server_id mismatch)")}catch(JZ){let WK=JZ instanceof Error?JZ:Error(String(JZ)),A9=AX(WK,500);return kZ(A9.headers),z8(A9,NQ,m$)}let f$=mQ.get(C$.sessionId,XZ);if(!f$){let JZ=AX(new B8("session not found, expired, or principal mismatch"),500);return kZ(JZ.headers),z8(JZ,NQ,m$)}X$=await f$.lock.acquire(),CX=f$.state,nJ=eW(C$.sessionId)}let _Z={acceptOpens:l8,state:CX,sessionId:nJ,mintToken:null,closed:!1,action:dJ?"resume":"none",_open(C$,f$){let{sessionId:JZ,expiresAt:WK}=mQ.open(C$,f$,XZ);_Z.sessionId=eW(JZ),_Z.state=C$,_Z.mintToken=cM(V,JZ,WK,G,VZ)},_close(){if(_Z.closed)return;let C$=_Z.sessionId;if(!C$)return;let f$=new Uint8Array(C$.length/2);for(let JZ=0;JZIW)VZ=IW;let l8=[];for(let C$=0;C$C$.expiresAt.getTime()),CX=YQ(TJ,{upload_url:l8.map((C$)=>C$.uploadUrl),download_url:l8.map((C$)=>C$.downloadUrl),expires_at:dJ}),nJ=f0(TJ,[CX]),_Z=fZ(nJ);return FZ(_Z.headers),kZ(_Z.headers),z8(_Z,NQ,m$)}catch(x){if(x instanceof w$){let U$=AX(x,x.statusCode,TJ);return kZ(U$.headers),z8(U$,NQ,m$)}let b0=AX(x,500,TJ);return kZ(b0.headers),z8(b0,NQ,m$)}}let{protocolName:zD,methodName:c6,action:qY}=i1,{binding:l6,method:OX}=M9,XK={...u6,protocolName:l6.name},qD=!l6.versionExempt&&l6.protocol.protocolVersionParts!==null;if(qY!=="exchange")try{let x;try{x=kQ(q8).metadata??void 0}catch{}if(x!==void 0)e(zD,x);if(qD)m(l6,x)}catch(x){let b0=OX.type===S$.UNARY?OX.resultSchema:g5,U$=q$(b0,x,V,null),XZ=f0(b0,[U$]),VZ=fZ(XZ,400);return FZ(VZ.headers),kZ(VZ.headers),z8(VZ,NQ,m$)}await d(S);let PD=OX.type===S$.UNARY?"unary":"stream",r1=u6.authContext,c8={method:c6,methodType:PD,serverId:V,requestId:L0,protocol:l6.name,protocolHash:await cW(l6),protocolVersion:o,kind:S,principal:r1?.principal??"",authDomain:r1?.domain??"",authenticated:r1?.authenticated??!1,requestData:qY==="call"||qY==="init"?q8:void 0,claims:r1?.claims,requestBytes:HD,deferral:i},FD={inputBatches:0,outputBatches:0,inputRows:0,outputRows:0,inputBytes:0,outputBytes:0},ND=p?.onDispatchStart(c8),JK;try{let x;if(qY==="call"){if(OX.type!==S$.UNARY)throw new w$(`Method '${c6}' is a stream method. Use /init and /exchange endpoints.`,400);x=await GM(OX,q8,XK)}else if(qY==="init"){if(OX.type!==S$.STREAM)throw new w$(`Method '${c6}' is a unary method. Use POST ${Y}/${c6} instead.`,400);x=await KM(OX,q8,XK)}else{if(OX.type!==S$.STREAM)throw new w$(`Method '${c6}' is a unary method. Use POST ${Y}/${c6} instead.`,400);x=await HM(OX,q8,XK)}let b0=x.__dispatchError;if(b0)JK=b0 instanceof Error?b0:Error(String(b0));return FZ(x.headers),kZ(x.headers),QK(x.headers,IZ),c8.httpStatus=x.status,z8(x,NQ,m$)}catch(x){if(JK=x instanceof Error?x:Error(String(x)),x instanceof w$){let U$=AX(x,x.statusCode);return kZ(U$.headers),QK(U$.headers,IZ),c8.httpStatus=U$.status,z8(U$,NQ,m$)}let b0=AX(x,500);return kZ(b0.headers),QK(b0.headers,IZ),c8.httpStatus=b0.status,z8(b0,NQ,m$)}finally{if(IZ){if(IZ.sessionId)c8.sessionId=IZ.sessionId;c8.sessionAction=IZ.action}if(oZ.streamId)c8.streamId=oZ.streamId;if(oZ.cancelled)c8.cancelled=!0;if(w0?.externalizedBytes)c8.externalizedBytes=w0.externalizedBytes;if(p?.onDispatchEnd(ND,c8,FD,JK),X$){try{X$()}catch{}X$=null}}};return async function(v){let i=YD(v);if(!p){let n$=await U9(v,void 0,void 0,i);return $K(n$),ZK(n$,i),n$}let w0=[],o0=await U9(v,{defer:(n$)=>w0.push(n$)},{externalizedBytes:0},i);if(w0.length===0)return $K(o0),ZK(o0,i),o0;let oZ;try{let n$=await GD(o0);oZ=n$.size,o0=n$.response}catch{}for(let n$ of w0)n$(oZ);return $K(o0),ZK(o0,i),o0};function $K(B){try{B.headers.set(hG,"true")}catch{}}function YD(B){let i=B.headers.get(lG)?.trim();if(i)return i;return crypto.randomUUID().replace(/-/g,"").slice(0,16)}function ZK(B,v){try{B.headers.set(lG,v)}catch{}}async function GD(B){let v=B.headers.get("Content-Length");if(v!==null){let w0=Number(v);if(Number.isFinite(w0)&&w0>=0)return{size:w0,response:B}}if(B.body===null)return{size:0,response:B};let i=new Uint8Array(await B.arrayBuffer());return{size:i.byteLength,response:new Response(i,{status:B.status,headers:B.headers})}}function QK(B,v){if(!v)return;if(v.mintToken!==null){B.set(fX,v.mintToken);for(let[i,w0]of LX)B.set(`${TW}${i}`,w0)}if(v.closed)B.set(_W,"true")}}var g5,sR,sM=8192;var iM=O(()=>{qZ();q6();z1();HZ();mW();M1();lX();R6();Y1();S1();mX();v7();BW();J8();PM();hM();mM();OJ();oM();w5();Gz();R1();g5=E0([]),sR=new Map});function v5($,Z){if($==null)return!1;try{return $ instanceof Z||Object.getPrototypeOf($)[Symbol.toStringTag]===Z.prototype[Symbol.toStringTag]}catch{return!1}}function ZZ($,Z,Q){let X=TypeError($,{cause:Q});return Object.assign(X,{code:Z}),X}function sX($){if(typeof $==="string")return $E.encode($);return ZE.decode($)}function v8($){if(typeof $==="string")return Pz($);return qz($)}function M0($,Z,Q){return new Lz($,{code:Z,cause:Q})}async function WL($){let Z;switch($.kty){case"EC":Z={crv:$.crv,kty:$.kty,x:$.x,y:$.y};break;case"OKP":Z={crv:$.crv,kty:$.kty,x:$.x};break;case"AKP":Z={alg:$.alg,kty:$.kty,pub:$.pub};break;case"RSA":Z={e:$.e,kty:$.kty,n:$.n};break;default:throw new PZ("unsupported JWK key type",{cause:$})}return v8(await crypto.subtle.digest("SHA-256",sX(JSON.stringify(Z))))}function YL($,Z){if(!($ instanceof CryptoKey))throw ZZ(`${Z} must be a CryptoKey`,iX)}function QE($,Z){if(YL($,Z),$.type!=="private")throw ZZ(`${Z} must be a private CryptoKey`,NX)}function XE($,Z){if(YL($,Z),$.type!=="public")throw ZZ(`${Z} must be a public CryptoKey`,NX)}function JE($){return $.toLowerCase().replace(/^application\//,"")}function hJ($){if($===null||typeof $!=="object"||Array.isArray($))return!1;return!0}function GL($){if(v5($,Headers))$=Object.fromEntries($.entries());let Z=new Headers($??{});if(zz&&!Z.has("user-agent"))Z.set("user-agent",zz);if(Z.has("authorization"))throw ZZ('"options.headers" must not include the "authorization" header name',NX);return Z}function KL($,Z){if(Z!==void 0){if(typeof Z==="function")Z=Z($.href);if(!(Z instanceof AbortSignal))throw ZZ('"options.signal" must return or be an instance of AbortSignal',iX);return Z}return}function HL($){if($.includes("//"))return $.replace("//","/");return $}function WE($,Z,Q=!1){if($.pathname==="/")$.pathname=Z;else $.pathname=HL(`${Z}/${Q?$.pathname:$.pathname.replace(/(\/)$/,"")}`);return $}function YE($,Z){return $.pathname=HL(`${$.pathname}/${Z}`),$}async function GE($,Z,Q,X){if(!($ instanceof URL))throw ZZ(`"${Z}" must be an instance of URL`,iX);UL($,X?.[XL]!==!0);let J=Q(new URL($.href)),W=GL(X?.headers);return W.set("accept","application/json"),(X?.[JL]||fetch)(J.href,{body:void 0,headers:Object.fromEntries(W.entries()),method:"GET",redirect:"manual",signal:KL(J,X?.signal)})}async function zL($,Z){return GE($,"issuerIdentifier",(Q)=>{switch(Z?.algorithm){case void 0:case"oidc":YE(Q,".well-known/openid-configuration");break;case"oauth2":WE(Q,".well-known/oauth-authorization-server");break;default:throw ZZ('"options.algorithm" must be "oidc" (default), or "oauth2"',NX)}return Q},Z)}function Az($,Z,Q,X){try{if(typeof $!=="string")throw ZZ(`${Z} must be a string`,iX,X);if($.length===0)throw ZZ(`${Z} must not be empty`,NX,X)}catch(J){if(Q)throw M0(J.message,Q,X);throw J}}async function qL($,Z){let Q=$;if(!(Q instanceof URL)&&Q!==QL)throw ZZ('"expectedIssuerIdentifier" must be an instance of URL',iX);if(!v5(Z,Response))throw ZZ('"response" must be an instance of Response',iX);if(Z.status!==200)throw M0('"response" is not a conform Authorization Server Metadata response (unexpected HTTP status code)',AL,Z);OL(Z);let X=await SL(Z);if(Az(X.issuer,'"response" body "issuer" property',$Z,{body:X}),Q!==QL&&new URL(X.issuer).href!==Q.href)throw M0('"response" body "issuer" property does not match the expected value',vE,{expected:Q.href,body:X,attribute:"issuer"});return X}function KE($){zE($,"application/json")}function PL($,...Z){let Q='"response" content-type must be ';if(Z.length>2){let X=Z.pop();Q+=`${Z.join(", ")}, or ${X}`}else if(Z.length===2)Q+=`${Z[0]} or ${Z[1]}`;else Q+=Z[0];return M0(Q,TE,$)}function HE($,...Z){if(!Z.includes(ML($)))throw PL($,...Z)}function zE($,Z){if(ML($)!==Z)throw PL($,Z)}function qE(){return v8(crypto.getRandomValues(new Uint8Array(32)))}function PE($){switch($.algorithm.hash.name){case"SHA-256":return"PS256";case"SHA-384":return"PS384";case"SHA-512":return"PS512";default:throw new PZ("unsupported RsaHashedKeyAlgorithm hash name",{cause:$})}}function FE($){switch($.algorithm.hash.name){case"SHA-256":return"RS256";case"SHA-384":return"RS384";case"SHA-512":return"RS512";default:throw new PZ("unsupported RsaHashedKeyAlgorithm hash name",{cause:$})}}function NE($){switch($.algorithm.namedCurve){case"P-256":return"ES256";case"P-384":return"ES384";case"P-521":return"ES512";default:throw new PZ("unsupported EcKeyAlgorithm namedCurve",{cause:$})}}function UE($){switch($.algorithm.name){case"RSA-PSS":return PE($);case"RSASSA-PKCS1-v1_5":return FE($);case"ECDSA":return NE($);case"Ed25519":case"ML-DSA-44":case"ML-DSA-65":case"ML-DSA-87":return $.algorithm.name;case"EdDSA":return"Ed25519";default:throw new PZ("unsupported CryptoKey algorithm name",{cause:$})}}function Dz($){let Z=$?.[aR];return typeof Z==="number"&&Number.isFinite(Z)?Z:0}function FL($){let Z=$?.[tR];return typeof Z==="number"&&Number.isFinite(Z)&&Math.sign(Z)!==-1?Z:30}function QY(){return Math.floor(Date.now()/1000)}function NL($){if(typeof $!=="object"||$===null)throw ZZ('"as" must be an object',iX);Az($.issuer,'"as.issuer"')}async function ME($,Z,Q){if(!Q.usages.includes("sign"))throw ZZ('CryptoKey instances used for signing assertions must include "sign" in their "usages"',NX);let X=`${v8(sX(JSON.stringify($)))}.${v8(sX(JSON.stringify(Z)))}`,J=v8(await crypto.subtle.sign(CL(Q),Q,sX(X)));return`${X}.${J}`}async function LE($,Z){let{kty:Q,e:X,n:J,x:W,y:Y,crv:G,pub:H}=await crypto.subtle.exportKey("jwk",$),z={kty:Q,e:X,n:J,x:W,y:Y,crv:G,pub:H};if(Q==="AKP")z.alg=Z;return Fz.set($,z),z}async function AE($,Z){return Fz||=new WeakMap,Fz.get($)||LE($,Z)}function UL($,Z){if(Z&&$.protocol!=="https:")throw M0("only requests to HTTPS are allowed",gE,$);if($.protocol!=="https:"&&$.protocol!=="http:")throw M0("only HTTP and HTTPS requests are allowed",xE,$)}function rM($,Z,Q,X){let J;if(typeof $!=="string"||!(J=DE($)))throw M0(`authorization server metadata does not contain a valid ${Q?`"as.mtls_endpoint_aliases.${Z}"`:`"as.${Z}"`}`,$===void 0?yE:hE,{attribute:Q?`mtls_endpoint_aliases.${Z}`:Z});return UL(J,X),J}function OE($,Z,Q,X){if(Q&&$.mtls_endpoint_aliases&&Z in $.mtls_endpoint_aliases)return rM($.mtls_endpoint_aliases[Z],Z,Q,X);return rM($[Z],Z,Q,X)}class CE{#Z;#Q;#X;#W;#Y;#$;#J;constructor($,Z,Q){if(QE(Z?.privateKey,'"DPoP.privateKey"'),XE(Z?.publicKey,'"DPoP.publicKey"'),!Z.publicKey.extractable)throw ZZ('"DPoP.publicKey.extractable" must be true',NX);this.#Y=Q?.[eR],this.#W=Dz($),this.#Q=Z.privateKey,this.#X=Z.publicKey,kE.add(this)}#G($){this.#$||=new Map;let Z=this.#$.get($);if(Z)this.#$.delete($),this.#$.set($,Z);return Z}#K($,Z){if(this.#$||=new Map,this.#$.delete($),this.#$.size===100)this.#$.delete(this.#$.keys().next().value);this.#$.set($,Z)}async calculateThumbprint(){if(!this.#J){let $=await crypto.subtle.exportKey("jwk",this.#X);this.#J||=await WL($)}return this.#J}async addProof($,Z,Q,X){let J=UE(this.#Q);this.#Z||={alg:J,typ:"dpop+jwt",jwk:await AE(this.#X,J)};let W=this.#G($.origin),G={iat:QY()+this.#W,jti:qE(),htm:Q,nonce:W,htu:`${$.origin}${$.pathname}`,ath:X?v8(await crypto.subtle.digest("SHA-256",sX(X))):void 0};this.#Y?.(this.#Z,G),Z.set("dpop",await ME(this.#Z,G,this.#Q))}cacheNonce($,Z){try{let Q=$.headers.get("dpop-nonce");if(Q)this.#K(Z.origin,Q)}catch{}}}function aM($,Z,Q,X){if(ZY||=new WeakMap,ZY.set($,{jwks:Z,uat:Q,get age(){return QY()-this.uat}}),X)Object.assign(X,{jwks:structuredClone(Z),uat:Q})}function wE($){if(typeof $!=="object"||$===null)return!1;if(!("uat"in $)||typeof $.uat!=="number"||QY()-$.uat>=300)return!1;if(!("jwks"in $)||!hJ($.jwks)||!Array.isArray($.jwks.keys)||!Array.prototype.every.call($.jwks.keys,hJ))return!1;return!0}function tM($,Z){ZY?.delete($),delete Z?.jwks,delete Z?.uat}async function Nz($,Z,Q){let{alg:X,kid:J}=Q;if(fE(Q),!ZY?.has($)&&wE(Z?.[$Y]))aM($,Z?.[$Y].jwks,Z?.[$Y].uat);let W,Y;if(ZY?.has($)){if({jwks:W,age:Y}=ZY.get($),Y>=300)return tM($,Z?.[$Y]),Nz($,Z,Q)}else W=await bE($,Z).then(mE),Y=0,aM($,W,QY(),Z?.[$Y]);let G;switch(X.slice(0,2)){case"RS":case"PS":G="RSA";break;case"ES":G="EC";break;case"Ed":G="OKP";break;case"ML":G="AKP";break;default:throw new PZ("unsupported JWS algorithm",{cause:{alg:X}})}let H=W.keys.filter((P)=>{if(P.kty!==G)return!1;if(J!==void 0&&J!==P.kid)return!1;if(P.alg!==void 0&&X!==P.alg)return!1;if(P.use!==void 0&&P.use!=="sig")return!1;if(P.key_ops?.includes("verify")===!1)return!1;switch(!0){case(X==="ES256"&&P.crv!=="P-256"):case(X==="ES384"&&P.crv!=="P-384"):case(X==="ES512"&&P.crv!=="P-521"):case(X==="Ed25519"&&P.crv!=="Ed25519"):case(X==="EdDSA"&&P.crv!=="Ed25519"):return!1}return!0}),{0:z,length:q}=H;if(!q){if(Y>=60)return tM($,Z?.[$Y]),Nz($,Z,Q);throw M0("error when selecting a JWT verification key, no applicable keys found",eM,{header:Q,candidates:H,jwks_uri:new URL($.jwks_uri)})}if(q!==1)throw M0('error when selecting a JWT verification key, multiple applicable keys found, a "kid" JWT Header Parameter is required',eM,{header:Q,candidates:H,jwks_uri:new URL($.jwks_uri)});return EL(X,z)}function ML($){return $.headers.get("content-type")?.split(";")[0]}function SE($,Z){if(Array.isArray(Z.claims.aud)){if(!Z.claims.aud.includes($))throw M0('unexpected JWT "aud" (audience) claim value',x6,{expected:$,claims:Z.claims,claim:"aud"})}else if(Z.claims.aud!==$)throw M0('unexpected JWT "aud" (audience) claim value',x6,{expected:$,claims:Z.claims,claim:"aud"});return Z}function BE($,Z){let Q=$[lE]?.(Z)??$.issuer;if(Z.claims.iss!==Q)throw M0('unexpected JWT "iss" (issuer) claim value',x6,{expected:Q,claims:Z.claims,claim:"iss"});return Z}function LL($,Z){for(let Q of $)if(Z.claims[Q]===void 0)throw M0(`JWT "${Q}" (${IE[Q]}) claim missing`,$Z,{claims:Z.claims});return Z}function DL($,Z){if(typeof Z.header.typ!=="string"||JE(Z.header.typ)!==$)throw M0('unexpected JWT "typ" header parameter value',$Z,{header:Z.header});return Z}function OL($){if($.bodyUsed)throw ZZ('"response" body has been used already',NX)}async function bE($,Z){NL($);let Q=OE($,"jwks_uri",!1,Z?.[XL]!==!0),X=GL(Z?.headers);return X.set("accept","application/json"),X.append("accept","application/jwk-set+json"),(Z?.[JL]||fetch)(Q.href,{body:void 0,headers:Object.fromEntries(X.entries()),method:"GET",redirect:"manual",signal:KL(Q,Z?.signal)})}async function mE($){if(!v5($,Response))throw ZZ('"response" must be an instance of Response',iX);if($.status!==200)throw M0('"response" is not a conform JSON Web Key Set response (unexpected HTTP status code)',AL,$);OL($);let Z=await SL($,(Q)=>HE(Q,"application/json","application/jwk-set+json"));if(!Array.isArray(Z.keys))throw M0('"response" body "keys" property must be an array',$Z,{body:Z});if(!Array.prototype.every.call(Z.keys,hJ))throw M0('"response" body "keys" property members must be JWK formatted objects',$Z,{body:Z});return Z}function Oz($){switch($){case"PS256":case"ES256":case"RS256":case"PS384":case"ES384":case"RS384":case"PS512":case"ES512":case"RS512":case"Ed25519":case"EdDSA":case"ML-DSA-44":case"ML-DSA-65":case"ML-DSA-87":return!0;default:return!1}}function fE($){if(!Oz($.alg))throw new PZ('unsupported JWS "alg" identifier',{cause:{alg:$.alg}})}function $L($){let{algorithm:Z}=$;if(typeof Z.modulusLength!=="number"||Z.modulusLength<2048)throw new PZ(`unsupported ${Z.name} modulusLength`,{cause:$})}function uE($){let{algorithm:Z}=$;switch(Z.namedCurve){case"P-256":return"SHA-256";case"P-384":return"SHA-384";case"P-521":return"SHA-512";default:throw new PZ("unsupported ECDSA namedCurve",{cause:$})}}function CL($){switch($.algorithm.name){case"ECDSA":return{name:$.algorithm.name,hash:uE($)};case"RSA-PSS":switch($L($),$.algorithm.hash.name){case"SHA-256":case"SHA-384":case"SHA-512":return{name:$.algorithm.name,saltLength:parseInt($.algorithm.hash.name.slice(-3),10)>>3};default:throw new PZ("unsupported RSA-PSS hash name",{cause:$})}case"RSASSA-PKCS1-v1_5":return $L($),$.algorithm.name;case"ML-DSA-44":case"ML-DSA-65":case"ML-DSA-87":case"Ed25519":return $.algorithm.name}throw new PZ("unsupported CryptoKey algorithm name",{cause:$})}async function jL($,Z,Q,X){let J=sX(`${$}.${Z}`),W=CL(Q);if(!await crypto.subtle.verify(W,Q,X,J))throw M0("JWT signature verification failed",$Z,{key:Q,data:J,signature:X,algorithm:W})}async function VL($,Z,Q,X,J){let{0:W,1:Y,length:G}=$.split(".");if(G===5)if(J!==void 0)$=await J($),{0:W,1:Y,length:G}=$.split(".");else throw new PZ("JWE decryption is not configured",{cause:$});if(G!==3)throw M0("Invalid JWT",$Z,$);let H;try{H=JSON.parse(sX(v8(W)))}catch(P){throw M0("failed to parse JWT Header body as base64url encoded JSON",Uz,P)}if(!hJ(H))throw M0("JWT Header must be a top level object",$Z,$);if(Z(H),H.crit!==void 0)throw new PZ('no JWT "crit" header parameter extensions are supported',{cause:{header:H}});let z;try{z=JSON.parse(sX(v8(Y)))}catch(P){throw M0("failed to parse JWT Payload body as base64url encoded JSON",Uz,P)}if(!hJ(z))throw M0("JWT Payload must be a top level object",$Z,$);let q=QY()+Q;if(z.exp!==void 0){if(typeof z.exp!=="number")throw M0('unexpected JWT "exp" (expiration time) claim type',$Z,{claims:z});if(z.exp<=q-X)throw M0('unexpected JWT "exp" (expiration time) claim value, expiration is past current timestamp',Mz,{claims:z,now:q,tolerance:X,claim:"exp"})}if(z.iat!==void 0){if(typeof z.iat!=="number")throw M0('unexpected JWT "iat" (issued at) claim type',$Z,{claims:z})}if(z.iss!==void 0){if(typeof z.iss!=="string")throw M0('unexpected JWT "iss" (issuer) claim type',$Z,{claims:z})}if(z.nbf!==void 0){if(typeof z.nbf!=="number")throw M0('unexpected JWT "nbf" (not before) claim type',$Z,{claims:z});if(z.nbf>q+X)throw M0('unexpected JWT "nbf" (not before) claim value',Mz,{claims:z,now:q,tolerance:X,claim:"nbf"})}if(z.aud!==void 0){if(typeof z.aud!=="string"&&!Array.isArray(z.aud))throw M0('unexpected JWT "aud" (audience) claim type',$Z,{claims:z})}return{header:H,claims:z,jwt:$}}function RL($,Z,Q,X){if($!==void 0){if(typeof $==="string"?X.alg!==$:!$.includes(X.alg))throw M0('unexpected JWT "alg" header parameter',$Z,{header:X,expected:$,reason:"client configuration"});return}if(Array.isArray(Z)){if(!Z.includes(X.alg))throw M0('unexpected JWT "alg" header parameter',$Z,{header:X,expected:Z,reason:"authorization server metadata"});return}if(Q!==void 0){if(typeof Q==="string"?X.alg!==Q:typeof Q==="function"?!Q(X.alg):!Q.includes(X.alg))throw M0('unexpected JWT "alg" header parameter',$Z,{header:X,expected:Q,reason:"default value"});return}throw M0('missing client or server configuration to verify used JWT "alg" header parameter',void 0,{client:$,issuer:Z,fallback:Q})}function pE($){switch($){case"PS256":case"PS384":case"PS512":return{name:"RSA-PSS",hash:`SHA-${$.slice(-3)}`};case"RS256":case"RS384":case"RS512":return{name:"RSASSA-PKCS1-v1_5",hash:`SHA-${$.slice(-3)}`};case"ES256":case"ES384":return{name:"ECDSA",namedCurve:`P-${$.slice(-3)}`};case"ES512":return{name:"ECDSA",namedCurve:"P-521"};case"EdDSA":return"Ed25519";case"Ed25519":case"ML-DSA-44":case"ML-DSA-65":case"ML-DSA-87":return $;default:throw new PZ("unsupported JWS algorithm",{cause:{alg:$}})}}async function EL($,Z){let{ext:Q,key_ops:X,use:J,...W}=Z;return crypto.subtle.importKey("jwk",W,pE($),!0,["verify"])}function Hz($){let Z=new URL($);return Z.search="",Z.hash="",Z.href}async function cE($,Z,Q,X){let J=$.headers.get("dpop");if(J===null)throw M0("operation indicated DPoP use but the request has no DPoP HTTP Header",FX,{headers:$.headers});if($.headers.get("authorization")?.toLowerCase().startsWith("dpop ")===!1)throw M0("operation indicated DPoP use but the request's Authorization HTTP Header scheme is not DPoP",FX,{headers:$.headers});if(typeof Q.cnf?.jkt!=="string")throw M0("operation indicated DPoP use but the JWT Access Token has no jkt confirmation claim",FX,{claims:Q});let W=Dz(X),Y=await VL(J,RL.bind(void 0,X?.signingAlgorithms,void 0,Oz),W,FL(X),void 0).then(DL.bind(void 0,"dpop+jwt")).then(LL.bind(void 0,["iat","jti","ath","htm","htu"])),G=QY()+W;if(Math.abs(G-Y.claims.iat)>300)throw M0("DPoP Proof iat is not recent enough",Mz,{now:G,claims:Y.claims,claim:"iat"});if(Y.claims.htm!==$.method)throw M0("DPoP Proof htm mismatch",x6,{expected:$.method,claims:Y.claims,claim:"htm"});if(typeof Y.claims.htu!=="string"||Hz(Y.claims.htu)!==Hz($.url))throw M0("DPoP Proof htu mismatch",x6,{expected:Hz($.url),claims:Y.claims,claim:"htu"});{let R=v8(await crypto.subtle.digest("SHA-256",sX(Z)));if(Y.claims.ath!==R)throw M0("DPoP Proof ath mismatch",x6,{expected:R,claims:Y.claims,claim:"ath"})}let{jwk:z,alg:q}=Y.header;if(!hJ(z))throw M0("DPoP Proof jwk header parameter must be a JSON object",FX,{header:Y.header});{let R=await WL(z);if(Q.cnf.jkt!==R)throw M0("JWT Access Token confirmation mismatch",x6,{expected:R,claims:Q,claim:"cnf.jkt"})}let{0:P,1:U,2:M}=J.split("."),L=v8(M),D=await EL(q,z);if(D.type!=="public")throw M0("DPoP Proof jwk header parameter must contain a public key",FX,{header:Y.header});await jL(P,U,D,L)}async function wL($,Z,Q,X){if(NL($),!v5(Z,Request))throw ZZ('"request" must be an instance of Request',iX);Az(Q,'"expectedAudience"');let J=Z.headers.get("authorization");if(J===null)throw M0('"request" is missing an Authorization HTTP Header',FX,{headers:Z.headers});let{0:W,1:Y,length:G}=J.split(" ");switch(W=W.toLowerCase(),W){case"dpop":case"bearer":break;default:throw new PZ("unsupported Authorization HTTP Header scheme",{cause:{headers:Z.headers}})}if(G!==2)throw M0("invalid Authorization HTTP Header format",FX,{headers:Z.headers});let H=["iss","exp","aud","sub","iat","jti","client_id"];if(X?.requireDPoP||W==="dpop"||Z.headers.has("dpop"))H.push("cnf");let{claims:z,header:q}=await VL(Y,RL.bind(void 0,X?.signingAlgorithms,void 0,Oz),Dz(X),FL(X),void 0).then(DL.bind(void 0,"at+jwt")).then(LL.bind(void 0,H)).then(BE.bind(void 0,$)).then(SE.bind(void 0,Q)).catch(ZL);for(let R of["client_id","jti","sub"])if(typeof z[R]!=="string")throw M0(`unexpected JWT "${R}" claim type`,FX,{claims:z});if("cnf"in z){if(!hJ(z.cnf))throw M0('unexpected JWT "cnf" (confirmation) claim value',FX,{claims:z});let{0:R,length:V}=Object.keys(z.cnf);if(V){if(V!==1)throw new PZ("multiple confirmation claims are not supported",{cause:{claims:z}});if(R!=="jkt")throw new PZ("unsupported JWT Confirmation method",{cause:{claims:z}})}}let{0:P,1:U,2:M}=Y.split("."),L=v8(M),D=await Nz($,X,q);if(await jL(P,U,D,L),X?.requireDPoP||W==="dpop"||z.cnf?.jkt!==void 0||Z.headers.has("dpop"))await cE(Z,Y,z,X).catch(ZL);return z}function ZL($){if($ instanceof Lz&&$?.code===FX)$.code=$Z;throw $}async function SL($,Z=KE){let Q;try{Q=await $.json()}catch(X){throw Z($),M0('failed to parse "response" body as JSON',Uz,X)}if(!hJ(Q))throw M0('"response" body must be a top level object',$Z,{body:Q});return Q}var zz,NX="ERR_INVALID_ARG_VALUE",iX="ERR_INVALID_ARG_TYPE",XL,aR,tR,JL,eR,bc,$Y,$E,ZE,qz,Pz,PZ,Lz,Fz,DE,x5="[a-zA-Z0-9!#$%&\\'\\*\\+\\-\\.\\^_`\\|~]+",jE="[a-zA-Z0-9\\-\\._\\~\\+\\/]+={0,2}",VE='"((?:[^"\\\\]|\\\\[\\s\\S])*)"',RE,EE,mc,fc,uc,pc,ZY,cc,kE,lc,IE,dc,nc,_E="OAUTH_UNSUPPORTED_OPERATION",Uz="OAUTH_PARSE_ERROR",$Z="OAUTH_INVALID_RESPONSE",FX="OAUTH_INVALID_REQUEST",TE="OAUTH_RESPONSE_IS_NOT_JSON",AL="OAUTH_RESPONSE_IS_NOT_CONFORM",gE="OAUTH_HTTP_REQUEST_FORBIDDEN",xE="OAUTH_REQUEST_PROTOCOL_FORBIDDEN",Mz="OAUTH_JWT_TIMESTAMP_CHECK_FAILED",x6="OAUTH_JWT_CLAIM_COMPARISON_FAILED",vE="OAUTH_JSON_ATTRIBUTE_COMPARISON_FAILED",eM="OAUTH_KEY_SELECTION_FAILED",yE="OAUTH_MISSING_SERVER_METADATA",hE="OAUTH_INVALID_SERVER_METADATA",oc,sc,QL,lE;var BL=O(()=>{if(typeof navigator>"u"||!navigator.userAgent?.startsWith?.("Mozilla/5.0 "))zz="oauth4webapi/v3.8.7";XL=Symbol(),aR=Symbol(),tR=Symbol(),JL=Symbol(),eR=Symbol(),bc=Symbol(),$Y=Symbol(),$E=new TextEncoder,ZE=new TextDecoder;if(Uint8Array.prototype.toBase64)qz=($)=>{if($ instanceof ArrayBuffer)$=new Uint8Array($);return $.toBase64({alphabet:"base64url",omitPadding:!0})};else qz=(Z)=>{if(Z instanceof ArrayBuffer)Z=new Uint8Array(Z);let Q=[];for(let X=0;X{try{return Uint8Array.fromBase64($,{alphabet:"base64url"})}catch(Z){throw ZZ("The input to be decoded is not correctly encoded.",NX,Z)}};else Pz=($)=>{try{let Z=atob($.replace(/-/g,"+").replace(/_/g,"/").replace(/\s/g,"")),Q=new Uint8Array(Z.length);for(let X=0;XURL.parse($,Z):($,Z)=>{try{return new URL($,Z)}catch{return null}};RE="("+x5+")\\s*=\\s*"+VE,EE="("+x5+")\\s*=\\s*("+x5+")",mc=new RegExp("^[,\\s]*("+x5+")"),fc=new RegExp("^[,\\s]*"+RE+"[,\\s]*(.*)"),uc=new RegExp("^[,\\s]*"+EE+"[,\\s]*(.*)"),pc=new RegExp("^("+jE+")(?:$|[,\\s])(.*)");cc=Symbol();kE=new WeakSet,lc=Symbol(),IE={aud:"audience",c_hash:"code hash",client_id:"client id",exp:"expiration time",iat:"issued at",iss:"issuer",jti:"jwt id",nonce:"nonce",s_hash:"state hash",sub:"subject",ath:"access token hash",htm:"http method",htu:"http uri",cnf:"confirmation",auth_time:"authentication time"};dc=Symbol(),nc=Symbol();oc=Symbol(),sc=Symbol();QL=Symbol(),lE=Symbol()});function Cz($){let Z=$.principalClaim??"sub",Q=$.domain??"jwt",X=$.audience,J=null;async function W(){if($.jwksUri)return{issuer:$.issuer,jwks_uri:$.jwksUri};let Y=new URL($.issuer),G=await zL(Y);return qL(Y,G)}return async function(G){if(!J)J=W();let H;try{H=await J}catch(M){throw J=null,M}let z=Array.isArray(X)?X:[X],q,P;for(let M of z)try{q=await wL(H,G,M);break}catch(L){P=L}if(!q)throw P;let U=q[Z]??null;return new zZ(Q,!0,U,q)}}var kL=O(()=>{BL();q6()});function _L(){let $=import.meta.require??globalThis.require??null;if(!$)throw Error("mTLS PEM-based authentication requires Node.js or Bun (node:crypto).");let Z=$(nE);return{X509Certificate:Z.X509Certificate,createHash:Z.createHash}}function IL($,Z){let Q=[],X=[],J=!1,W=0;while(W<$.length){let Y=$[W];if(Y==='"')J=!J,X.push(Y);else if(Y==="\\"&&J&&W+1<$.length)X.push(Y),X.push($[W+1]),W++;else if(Y===Z&&!J)Q.push(X.join("")),X=[];else X.push(Y);W++}return Q.push(X.join("")),Q}function oE($){return $.replace(/\\(.)/g,"$1")}function sE($){for(let Z of $.split(/(?=2&&P[0]==='"'&&P[P.length-1]==='"')P=oE(P.slice(1,-1));if(q==="cert"||q==="uri"||q==="by")P=decodeURIComponent(P);if(q==="dns"){let U=W.dns;if(Array.isArray(U))U.push(P);else W.dns=[P]}else W[q]=P}let Y=Array.isArray(W.dns)?W.dns:[];Z.push({hash:typeof W.hash==="string"?W.hash:null,cert:typeof W.cert==="string"?W.cert:null,subject:typeof W.subject==="string"?W.subject:null,uri:typeof W.uri==="string"?W.uri:null,dns:Y,by:typeof W.by==="string"?W.by:null})}return Z}function jz($){let Z=$?.validate,Q=$?.domain??"mtls",X=$?.selectElement??"first";return async function(W){let Y=W.headers.get("x-forwarded-client-cert");if(!Y)throw Error("Missing x-forwarded-client-cert header");let G=y5(Y);if(G.length===0)throw Error("Empty x-forwarded-client-cert header");let H=X==="first"?G[0]:G[G.length-1];if(Z)return Z(H);let z=H.subject?sE(H.subject):"",q={};if(H.hash)q.hash=H.hash;if(H.subject)q.subject=H.subject;if(H.uri)q.uri=H.uri;if(H.dns.length>0)q.dns=[...H.dns];if(H.by)q.by=H.by;return new zZ(Q,!0,z,q)}}function iE($,Z){let Q=$.headers.get(Z);if(!Q)throw Error(`Missing ${Z} header`);let X=decodeURIComponent(Q);if(!X.startsWith("-----BEGIN CERTIFICATE-----"))throw Error("Header value is not a PEM certificate");let{X509Certificate:J}=_L();try{return new J(X)}catch(W){throw Error(`Failed to parse PEM certificate: ${W}`)}}function rE($){let Z=new Date,Q=new Date($.validFrom),X=new Date($.validTo);if(ZX)throw Error("Certificate has expired")}function h1($){let{validate:Z,header:Q="X-SSL-Client-Cert",checkExpiry:X=!1}=$;return async function(W){let Y=iE(W,Q);if(X)rE(Y);return Z(Y)}}function Vz($){let{fingerprints:Z,header:Q,algorithm:X="sha256",checkExpiry:J}=$;if(!aE.has(X))throw Error(`Unsupported hash algorithm: ${X}`);let W=Z instanceof Map?Z:new Map(Object.entries(Z));function Y(G){let{createHash:H}=_L(),z=H(X).update(G.raw).digest("hex"),q=W.get(z);if(!q)throw Error(`Unknown certificate fingerprint: ${z}`);return q}return h1({validate:Y,header:Q,checkExpiry:J})}function Rz($){let{header:Z,domain:Q="mtls",allowedSubjects:X=null,checkExpiry:J}=$??{};function W(Y){let G=Y.subject.split(` -`).map((U)=>U.trim()).filter(Boolean),H=G.join(", "),z="";for(let U of G)if(U.toUpperCase().startsWith("CN=")){z=U.slice(3);break}if(X!==null&&!X.has(z))throw Error(`Subject CN '${z}' not in allowed subjects`);let q=BigInt(`0x${Y.serialNumber}`).toString(16),P=new Date(Y.validTo).toISOString();return new zZ(Q,!0,z,{subject_dn:H,serial:q,not_valid_after:P})}return h1({validate:W,header:Z,checkExpiry:J})}var nE="node:crypto",aE;var TL=O(()=>{q6();aE=new Set(["sha256","sha1","sha384","sha512"])});function v6($){let Z=xL($);if(Z)return Z.join(".");if(!$.includes(":")||$.includes("%")||$.includes("[")||$.includes("]"))return null;let Q=$.toLowerCase().split("::");if(Q.length>2)return null;let X=gL(Q[0],Q.length===1),J=gL(Q.length===2?Q[1]:"",!0);if(!X||!J)return null;let W;if(Q.length===1){if(X.length!==8)return null;W=X}else{let q=8-X.length-J.length;if(q<1)return null;W=[...X,...Array(q).fill(0),...J]}if(W.length!==8)return null;if(W.slice(0,5).every((q)=>q===0)&&W[5]===65535)return`${W[6]>>>8}.${W[6]&255}.${W[7]>>>8}.${W[7]&255}`;let Y=-1,G=0;for(let q=0;qG&&P-q>=2)Y=q,G=P-q;q=P}if(Y<0)return W.map((q)=>q.toString(16)).join(":");let H=W.slice(0,Y).map((q)=>q.toString(16)).join(":"),z=W.slice(Y+G).map((q)=>q.toString(16)).join(":");return`${H}::${z}`}function h5($,Z){let Q=new Set,X=0;for(let J of $){X++;let W=v6(J);if(!W)throw TypeError(`${Z} must contain exact IP literals`);if(Q.has(W))throw TypeError(`${Z} contains a duplicate normalized address`);Q.add(W)}if(X===0)throw TypeError(`${Z} must not be empty`);return Q}function xL($){let Z=$.split(".");if(Z.length!==4)return null;let Q=[];for(let X of Z){if(!/^(?:0|[1-9][0-9]{0,2})$/u.test(X))return null;let J=Number(X);if(J>255)return null;Q.push(J)}return Q}function gL($,Z){if($==="")return[];let Q=$.split(":"),X=[];for(let J=0;J{let $=import.meta.require??globalThis.require??null;if($)return $(vL).X509Certificate;let Z=await import(vL);if(!Z.X509Certificate)throw Error("node:crypto X509Certificate is unavailable");return Z.X509Certificate})(),yL}function m1($){for(let Z=0;Z<$.length;Z++)if($.charCodeAt(Z)>127)return!1;return!0}function Ez($){for(let Z=0;Z<$.length;Z++){let Q=$.charCodeAt(Z);if(Q<=31||Q===127)return!0}return!1}function b1($){return new TextEncoder().encode($).length}function wz($){if($.length%2!==0)throw new A$("rawHeaders contains an unmatched name");let Z=new Map;for(let Q=0;Q<$.length;Q+=2){let X=$[Q],J=$[Q+1],W=Z.get(X)??[];W.push(J),Z.set(X,W)}return new Map([...Z].map(([Q,X])=>[Q,Object.freeze(X)]))}function Sz($,Z){let Q=new Set($),X=h5(Z,"trustedProxyAddresses");if(Q.size===0)throw TypeError("trustDomains and trustedProxyAddresses must not be empty");for(let J of Q)if(!mL.test(J))throw TypeError(`invalid SPIFFE trust domain: ${JSON.stringify(J)}`);return{domains:Q,proxies:X}}function b5($,Z){if(!eE.test($))throw TypeError(`${Z} must be a valid HTTP field name`)}function WY($,Z){if(!$||!m1($)||b1($)>2048||$.includes("%"))throw TypeError("SPIFFE ID is empty, non-ASCII, percent-encoded, or exceeds 2048 bytes");if($.includes("?")||$.includes("#"))throw TypeError("SPIFFE ID cannot contain query or fragment");let Q=/^spiffe:\/\/([^/]+)(\/.*)$/.exec($);if(!Q)throw TypeError("invalid SPIFFE ID scheme or authority");let X=Q[1],J=Q[2];if(!mL.test(X)||!tE.test(J))throw TypeError("SPIFFE ID trust domain or path is not canonical");if(J.split("/").some((W)=>W==="."||W===".."))throw TypeError("SPIFFE ID path cannot contain dot segments");if(!Z.has(X))throw TypeError("SPIFFE trust domain is not allowed");return X}function XY($,Z){if(Z+2>$.length)throw TypeError("truncated DER value");let Q=$[Z],X=$[Z+1],J=0,W=2;if((X&128)===0)J=X;else{let H=X&127;if(H===0||H>4||Z+2+H>$.length)throw TypeError("invalid DER length");W+=H;for(let z=0;z$.length)throw TypeError("truncated DER body");return{tag:Q,start:Y,end:G,bytes:$}}function bJ($){let Z=[],Q=$.start;while(Q<$.end){let X=XY($.bytes,Q);if(X.end>$.end)throw TypeError("DER child exceeds parent");Z.push(X),Q=X.end}if(Q!==$.end)throw TypeError("malformed DER children");return Z}function JY($){return $.bytes.subarray($.start,$.end)}function hL($){if($.tag!==6)throw TypeError("expected DER OID");let Z=JY($);if(Z.length===0)throw TypeError("empty DER OID");let Q=[Math.min(2,Math.floor(Z[0]/40)),0];Q[1]=Z[0]-Q[0]*40;let X=0;for(let J=1;Jy.tag===163);if(!G)throw TypeError("X.509-SVID extensions are missing");let H=bJ(G);if(H.length!==1||H[0].tag!==48)throw TypeError("invalid certificate extensions");let z=new Map;for(let y of bJ(H[0])){if(y.tag!==48)throw TypeError("invalid certificate extension");let h=bJ(y);if(h.length<2||h.length>3)throw TypeError("invalid certificate extension shape");let s=hL(h[0]),f=!1,c=h[1];if(h[1].tag===1){let o=JY(h[1]);if(o.length!==1||o[0]!==0&&o[0]!==255||h.length!==3)throw TypeError("invalid extension critical flag");f=o[0]===255,c=h[2]}if(c.tag!==4||z.has(s))throw TypeError("invalid or duplicate extension");z.set(s,{critical:f,value:JY(c)})}let q=z.get("2.5.29.17"),P=z.get("2.5.29.19"),U=z.get("2.5.29.15");if(!q||!P||!U)throw TypeError("required X.509-SVID extension is missing");let M=XY(q.value,0);if(M.tag!==48||M.end!==q.value.length)throw TypeError("invalid SAN extension");let L=bJ(M).filter((y)=>y.tag===134).map((y)=>{let h=new TextDecoder("ascii",{fatal:!0}).decode(JY(y));if(!m1(h))throw TypeError("non-ASCII URI SAN");return h});if(L.length!==1)throw TypeError("X.509-SVID must contain exactly one URI SAN");let D=XY(P.value,0);if(D.tag!==48||D.end!==P.value.length)throw TypeError("invalid basic constraints");let R=bJ(D),V=R[0]?.tag===1&&JY(R[0])[0]!==0,g=XY(U.value,0);if(g.tag!==3||g.end!==U.value.length)throw TypeError("invalid key usage");let w=JY(g);if(w.length<2||w[0]>7)throw TypeError("invalid key usage bits");let T=(y)=>{let h=w[1+Math.floor(y/8)];return h!==void 0&&(h&128>>y%8)!==0},E=z.get("2.5.29.37"),_;if(E){let y=XY(E.value,0);if(y.tag!==48||y.end!==E.value.length)throw TypeError("invalid extended key usage");_=new Set(bJ(y).map(hL))}return{spiffeId:L[0],subjectEmpty:Y,sanCritical:q.critical,ca:V,keyUsageCritical:U.critical,digitalSignature:T(0),keyCertSign:T(5),crlSign:T(6),extendedKeyUsage:_}}function Ww($,Z,Q,X){let J=Date.now(),W=Date.parse($.validFrom),Y=Date.parse($.validTo);if(!Number.isFinite(W)||!Number.isFinite(Y)||JY)throw TypeError("X.509-SVID is outside its validity period");let G=Jw(new Uint8Array($.raw));if(G.subjectEmpty&&!G.sanCritical)throw TypeError("subjectless SVID requires critical SAN");if(G.ca)throw TypeError("X.509-SVID leaf cannot be a CA");if(!G.keyUsageCritical||!G.digitalSignature||G.keyCertSign||G.crlSign)throw TypeError("invalid X.509-SVID key usage");if(G.extendedKeyUsage&&(!G.extendedKeyUsage.has("1.3.6.1.5.5.7.3.1")||!G.extendedKeyUsage.has("1.3.6.1.5.5.7.3.2")))throw TypeError("X.509-SVID extended key usage must include clientAuth and serverAuth");let H=WY(G.spiffeId,Z);return new pX({provider:QZ,evidenceSource:X,assurance:KX.CONFIGURED_PROXY,issuer:`spiffe://${H}`,transport:"http",subjectKind:HX.WORKLOAD,subjectKey:G.spiffeId,subjectStability:W8.STABLE,subjectVerified:!0,sourceAddress:Q.assertedPeer,proxyAddress:Q.immediatePeer})}function m5($){let{domains:Z,proxies:Q}=Sz($.trustDomains,$.trustedProxyAddresses);if(b5($.certificateHeader,"certificateHeader"),$.verificationHeader!==void 0){if(b5($.verificationHeader,"verificationHeader"),$.certificateHeader.toLowerCase()===$.verificationHeader.toLowerCase())throw TypeError("certificate and verification headers must be distinct");if(Ez($.verificationValue??""))throw TypeError("invalid verification value")}if(!Number.isSafeInteger($.maxHeaderBytes)||$.maxHeaderBytes<=0)throw TypeError("maxHeaderBytes must be a positive integer");return{provider:QZ,async resolve(X){let J=v6(X.immediatePeer??"");if(!J||!Q.has(J))return new P$(QZ,C0.UNTRUSTED_PROXY);let W,Y;try{if(W=X.header($.certificateHeader),$.verificationHeader)Y=X.header($.verificationHeader)}catch(G){if(G instanceof A$)return new P$(QZ,C0.INVALID);throw G}if(!W)return new P$(QZ,C0.NO_MATCH);if(!m1(W)||b1(W)>$.maxHeaderBytes)return new P$(QZ,C0.INVALID);if($.verificationHeader&&(Y===void 0||b1(Y)>64||Y!==$.verificationValue))return new P$(QZ,C0.INVALID);try{let G=decodeURIComponent(W);if(!m1(G)||b1(G)>$.maxHeaderBytes||G.split("-----BEGIN CERTIFICATE-----").length-1!==1||G.split("-----END CERTIFICATE-----").length-1!==1||!G.trim().endsWith("-----END CERTIFICATE-----"))throw TypeError("invalid certificate header");let H=await Xw(),z=Ww(new H(G),Z,X,$.evidenceSource);return P$.available(z)}catch{return new P$(QZ,C0.INVALID)}}}}function Bz($){if(!$.chainVerifiedHeader)throw TypeError("chainVerifiedHeader is required");return m5({trustDomains:$.trustDomains,trustedProxyAddresses:$.trustedProxyAddresses,certificateHeader:$.header??"X-SSL-Client-Cert",verificationHeader:$.chainVerifiedHeader,verificationValue:$.chainVerifiedValue??"true",maxHeaderBytes:$.maxHeaderBytes??16384,evidenceSource:"verified_certificate_header"})}function kz($){return m5({trustDomains:$.trustDomains,trustedProxyAddresses:$.trustedProxyAddresses,certificateHeader:$.certificateHeader??"X-SSL-Client-Cert",verificationHeader:$.verificationHeader??"X-SSL-Client-Verify",verificationValue:"SUCCESS",maxHeaderBytes:$.maxHeaderBytes??16384,evidenceSource:"nginx_mtls"})}function Iz($){return m5({trustDomains:$.trustDomains,trustedProxyAddresses:$.trustedProxyAddresses,certificateHeader:$.certificateHeader??"X-Client-Certificate",verificationHeader:$.verificationHeader??"X-Client-Certificate-Verification",verificationValue:"SUCCESS",maxHeaderBytes:$.maxHeaderBytes??16384,evidenceSource:"azure_application_gateway_mtls_strict"})}function _z($){return m5({trustDomains:$.trustDomains,trustedProxyAddresses:$.trustedProxyAddresses,certificateHeader:$.leafHeader??"X-Amzn-Mtls-Clientcert-Leaf",maxHeaderBytes:$.maxHeaderBytes??16384,evidenceSource:"aws_alb_mtls_verify"})}function Tz($){let{domains:Z,proxies:Q}=Sz($.trustDomains,$.trustedProxyAddresses),X={spiffe:$.spiffeIdHeader??"X-Client-Cert-Spiffe-Id",present:$.presentHeader??"X-Client-Cert-Present",verified:$.chainVerifiedHeader??"X-Client-Cert-Chain-Verified",error:$.errorHeader??"X-Client-Cert-Error"};for(let[J,W]of Object.entries(X))b5(W,J);if(new Set(Object.values(X).map((J)=>J.toLowerCase())).size!==4)throw TypeError("GCP mTLS header names must be distinct");return{provider:QZ,resolve(J){let W=v6(J.immediatePeer??"");if(!W||!Q.has(W))return new P$(QZ,C0.UNTRUSTED_PROXY);try{let Y=J.header(X.present),G=J.header(X.verified),H=J.header(X.spiffe),z=J.header(X.error);if(Y==="false"&&(G===void 0||G==="false")&&H===void 0)return new P$(QZ,C0.NO_MATCH);if(Y!=="true"||G!=="true"||z!==void 0&&z!==""||!H)return new P$(QZ,C0.INVALID);let q=WY(H,Z);return P$.available(new pX({provider:QZ,evidenceSource:"gcp_load_balancer_mtls",assurance:KX.CONFIGURED_PROXY,issuer:`spiffe://${q}`,transport:"http",subjectKind:HX.WORKLOAD,subjectKey:H,subjectStability:W8.STABLE,subjectVerified:!0,attributes:{client_certificate_present:!0,client_certificate_chain_verified:!0},sourceAddress:J.assertedPeer,proxyAddress:J.immediatePeer}))}catch{return new P$(QZ,C0.INVALID)}}}}function bL($,Z){let Q=[],X="",J=!1,W=!1;for(let Y of $)if(W){if(Y!=='"'&&Y!=="\\")throw TypeError("unsupported XFCC quoted escape");X+=Y,W=!1}else if(J&&Y==="\\")W=!0;else if(Y==='"')J=!J,X+=Y;else if(Y===Z&&!J)Q.push(X),X="";else X+=Y;if(J||W)throw TypeError("unterminated XFCC quoted value");return Q.push(X),Q}function Yw($){if($.startsWith('"')||$.endsWith('"')){if($.length<2||!$.startsWith('"')||!$.endsWith('"'))throw TypeError("malformed quoted XFCC");return $.slice(1,-1)}if(!$||/[,;=]/.test($))throw TypeError("invalid unquoted XFCC value");return $}function Gw($){if($.replace(Qw,"").includes("%"))throw TypeError("invalid XFCC percent escape");let Z=decodeURIComponent($);if(Ez(Z))throw TypeError("decoded XFCC value contains controls");return Z}function Kw($,Z){if(!m1($)||b1($)>Z||Ez($))throw TypeError("invalid XFCC bytes");let Q=bL($,",");if(Q.length!==1||!Q[0].trim())throw TypeError("XFCC must contain one element");let X=new Map;for(let J of bL(Q[0],";")){let W=J.trim(),Y=W.indexOf("=");if(!W||Y<0)throw TypeError("malformed XFCC field");let G=W.slice(0,Y).trim(),H=G.toLowerCase();if(!$w.test(G)||!["by","hash","cert","chain","subject","uri","dns","issuer"].includes(H))throw TypeError("unsupported XFCC field");let z=Yw(W.slice(Y+1).trim());if(["by","uri","cert","chain"].includes(H))z=Gw(z);if(!["by","uri","dns"].includes(H)&&X.has(H))throw TypeError("duplicate XFCC singleton");let q=X.get(H)??[];q.push(z),X.set(H,q)}return X}function gz($){let{domains:Z,proxies:Q}=Sz($.trustDomains,$.trustedProxyAddresses),X=$.header??"X-Forwarded-Client-Cert",J=$.maxHeaderBytes??16384;if(b5(X,"header"),!Number.isSafeInteger(J)||J<=0)throw TypeError("maxHeaderBytes must be positive");return{provider:QZ,resolve(W){let Y=v6(W.immediatePeer??"");if(!Y||!Q.has(Y))return new P$(QZ,C0.UNTRUSTED_PROXY);try{let G=W.header(X);if(G===void 0)return new P$(QZ,C0.NO_MATCH);let H=Kw(G,J),z=H.get("uri")??[],q=H.get("hash")??[];if(z.length!==1||q.length!==1||!Zw.test(q[0]))throw TypeError("ambiguous XFCC identity");let P=WY(z[0],Z),U=H.get("by")??[];return P$.available(new pX({provider:QZ,evidenceSource:"envoy_xfcc_sanitize_set",assurance:KX.CONFIGURED_PROXY,issuer:`spiffe://${P}`,transport:"http",subjectKind:HX.WORKLOAD,subjectKey:z[0],subjectStability:W8.STABLE,subjectVerified:!0,attributes:{certificate_sha256:q[0].toLowerCase(),...U.length>0?{proxy_identities:U}:{}},sourceAddress:W.assertedPeer,proxyAddress:W.immediatePeer}))}catch{return new P$(QZ,C0.INVALID)}}}}var QZ="spiffe",mL,tE,eE,$w,Zw,Qw,vL="node:crypto",yL;var fL=O(()=>{mW();mL=/^[a-z0-9](?:[a-z0-9._-]{0,253}[a-z0-9])?$/,tE=/^\/(?:[A-Za-z0-9._-]+)(?:\/[A-Za-z0-9._-]+)*$/,eE=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/,$w=/^[A-Za-z][A-Za-z0-9_-]*$/,Zw=/^[0-9A-Fa-f]{64}$/,Qw=/%(?:[0-9A-Fa-f]{2})/g});var uL=O(()=>{v7();J8();iM();kL();TL();fL();w5();Gz();R1()});function pL($){return{peerIdentityProviders:[yz({issuer:$.issuer,trustedProxyAddresses:$.trustedProxyAddresses??["127.0.0.1"]})],peerAuthenticationPolicy:$.authenticate===!1?Z5:bW("iroh"),peerResolutionContext:$.peerResolutionContext}}function zw($){if(typeof $!=="string"||!$)throw TypeError("Iroh issuer must be non-empty text without controls");for(let Z=0;Z<$.length;Z++){let Q=$.charCodeAt(Z);if(Q>=55296&&Q<=56319){let X=$.charCodeAt(Z+1);if(!(X>=56320&&X<=57343))throw TypeError("Iroh issuer contains an unpaired surrogate");Z++}else if(Q>=56320&&Q<=57343)throw TypeError("Iroh issuer contains an unpaired surrogate")}if(Array.from($).some((Z)=>{let Q=Z.codePointAt(0);return Q<=31||Q===127}))throw TypeError("Iroh issuer must be non-empty text without controls")}function yz($){zw($.issuer);let Z=h5($.trustedProxyAddresses,"Iroh trustedProxyAddresses"),Q=(X,J)=>new P$(xz,X,J?[J]:[]);return{provider:xz,resolve(X){let J=X?v6(X.immediatePeer??""):null;if(!J||!Z.has(J))return Q(C0.UNTRUSTED_PROXY);try{let W=X.header(vz);if(W===void 0)return Q(C0.NO_MATCH);if(!Hw.test(W))return Q(C0.INVALID);return Q(C0.AVAILABLE,new pX({provider:xz,evidenceSource:"http_proxy",assurance:KX.CONFIGURED_PROXY,issuer:$.issuer,transport:"http",subjectKind:HX.ENDPOINT,subjectKey:W,subjectStability:W8.STABLE,subjectVerified:!0,attributes:{original_assurance:KX.CRYPTOGRAPHIC_PEER},sourceAddress:W,proxyAddress:J}))}catch{return Q(C0.INVALID)}}}}var xz="iroh",Hw,vz="VGI-Forwarded-Iroh-Endpoint";var cL=O(()=>{mW();Hw=/^[0-9a-f]{64}$/u});async function lL($,Z,Q,X,J,W,Y,G,H,z){let q=!!$.producerFn,P;try{if(q)P=await $.producerInit(Z);else P=await $.exchangeInit(Z)}catch(V){let g=$.headerSchema??qw,w=q$(g,V,J,W);if(await Q.writeStream(g,[w]),await X.openNextStream())while(await X.readNextBatch()!==null);return}let U=P?.__outputSchema??$.outputSchema,M=P?.__isProducer??q;if($.headerSchema&&$.headerInit)try{let V=new xQ($.headerSchema,!0,J,W,H,void 0,G,{peerEvidence:z}),g=$.headerInit(Z,P,V),w=D6($.headerSchema,g,J,W),T=[...V.batches.map((E)=>E.batch),w];await Q.writeStream($.headerSchema,T)}catch(V){let g=q$($.headerSchema,V,J,W);if(await Q.writeStream($.headerSchema,[g]),await X.openNextStream())while(await X.readNextBatch()!==null);return}if(!await X.openNextStream()){let V=q$(U,Error("Expected input stream but got EOF"),J,W);await Q.writeStream(U,[V]);return}let D=Q.openStream(U),R=P?.__inputSchema??$.inputSchema;try{while(!0){let V=await X.readNextBatch();if(!V)break;if(V.metadata?.get(bX)){if($.onCancel)try{await $.onCancel(P)}catch(w){console.debug?.(`onCancel hook failed: ${w instanceof Error?w.message:w}`)}break}if(R&&!M&&V.schema!==R)try{V=U6(V,R)}catch(w){if(w instanceof TypeError)throw w;console.debug?.(`Schema conformance skipped: ${w instanceof Error?w.message:w}`)}let g=new xQ(U,M,J,W,H,void 0,G,{inputMetadata:V.metadata??void 0,peerEvidence:z});if(q)await $.producerFn(P,g);else await $.exchangeFn(P,V,g);for(let w of g.batches){let T=w.batch;if(w.metadata&&w.metadata.size>0){let E=new Map(T.metadata??[]);for(let[_,y]of w.metadata)E.set(_,y);T=e$(T,E)}if(Y)T=await kJ(T,Y);await D.write(T)}if(g.finished)break}}catch(V){await D.write(q$(U,V,J,W))}await D.close();try{while(await X.readNextBatch()!==null);}catch{}}var qw;var dL=O(()=>{qZ();I8();lX();mX();qw=E0([])});async function nL($,Z,Q,X,J,W,Y,G,H){let z=$.resultSchema,q=new xQ(z,!0,X,J,G,void 0,Y,{peerEvidence:H});try{let P=await $.handler(Z,q),U=D6(z,P,X,J);if(W)U=await kJ(U,W);let M=[...q.batches.map((L)=>L.batch),U];await Q.writeStream(z,M)}catch(P){let U=q$(z,P,X,J);await Q.writeStream(z,[U])}}var oL=O(()=>{I8();lX();mX()});async function iL($){return eG(new TextEncoder().encode($))}function fz($){let Z=new Set([...$??[]].filter((Q)=>Q));if(Z.size===0)throw Error("introspectPrincipals must name at least one principal. Introspection is a distinct capability from authentication: allowing any authenticated caller lets any user resolve any other user's credential to its owner.");return Z}function uz($,Z){let Q=$.principal??"";if(!$.authenticated||!Z.has(Q))throw new u1("caller is not an introspector");return Q}function sL($){return Fw.has($)||$.trim()===""}function p5($){let Z=0,Q=$.length;while(ZZ&&sL($[Q-1]))Q--;return $.slice(Z,Q)}function pz($){return Pw.test(p5($))}function cz($){if(!p5($)||lz($)>hz||pz($))throw new p1("unresolved")}function lz($){return new TextEncoder().encode($).length}function dz($,Z,Q){if(!$.authenticated||!$.principal)throw new y6("caller is not authenticated");let X=$.claims?.auth_time;if(X===void 0||X===null)throw new y6("credential carries no auth_time; only a recently authenticated user may mint a grant");let J=typeof X==="number"?X:Number(X);if(!Number.isFinite(J))throw new y6("credential carries an unusable auth_time");let W=(Q??Date.now()/1000)-J;if(W>Z)throw new y6(`last authentication was ${W.toFixed(0)}s ago, which exceeds the ${Z.toFixed(0)}s ceiling for minting a grant; re-authenticate`);return J}function sz($){return mZ(YQ(nz,{principal:[$.principal],token_name:[$.tokenName??""],ttl_seconds:[BigInt(Math.trunc($.ttlSeconds??bz))]}))}function iz($){return mZ(YQ(oz,{token:[$.token],expires_at:[$.expiresAt],grant_id:[$.grantId??""]}))}class rz{resolveTokenHook;mintGrantHook;principals;maxAuthAge;constructor($={}){this.resolveTokenHook=$.resolveToken,this.mintGrantHook=$.mintGrant,this.maxAuthAge=$.maxAuthAge??mz,this.principals=this.resolveTokenHook?fz($.introspectPrincipals):new Set}offeredMethods(){let $=new Set;if(this.resolveTokenHook)$.add("introspect_token");if(this.mintGrantHook)$.add("issue_grant");return $}async introspectToken($,Z){if(!this.resolveTokenHook)throw new u1("this worker does not resolve credentials");uz(Z,this.principals),cz($);let Q;try{Q=await this.resolveTokenHook($)}catch(X){if(X instanceof qX)throw new u5(X.detail,X.retryAfter);throw X}if(Q==null)throw new p1("unresolved");return Q}async issueGrant($,Z,Q,X){if(!this.mintGrantHook)throw new f5("this worker does not mint grants");return dz(X,this.maxAuthAge),this.mintGrantHook(X.principal??"",$,Z,Q)}}function Nw($){if($==null)return[];return(Array.isArray($)?$:[...$]).map((Q)=>Q==null?"":String(Q))}function c5($){let Z=$.offeredMethods();if(Z.size===0)return null;let Q=new E6(f1);if(Z.has("introspect_token"))Q.unary("introspect_token",{params:{token:Z$()},result:{result:X8()},doc:"Resolve an opaque bearer credential to the identity it authenticates as.",handler:async(X,J)=>{let W=J.auth;return{result:sz(await $.introspectToken(String(X.token??""),W))}}});if(Z.has("issue_grant"))Q.unary("issue_grant",{params:{purpose:Z$(),scopes:P6(O0("item",Z$(),!0)),ttl_seconds:VJ()},result:{result:X8()},doc:"Mint a standing delegation credential for the calling user.",handler:async(X,J)=>{let W=J.auth,Y=await $.issueGrant(String(X.purpose??""),Nw(X.scopes),Number(X.ttl_seconds??0),W);return{result:iz(Y)}}});return Q}var f1="vgi_rpc.Identity.v1",Pw,hz=4096,bz=300,mz=900,u1,p1,y6,f5,u5,Fw,nz,oz;var az=O(()=>{qZ();R1();X5();R6();Pw=/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;u1=class u1 extends Error{static errorKind="introspection_refused";errorKind="introspection_refused";constructor($){super($);this.name="IntrospectionRefusedError"}};p1=class p1 extends Error{static errorKind="token_unresolved";errorKind="token_unresolved";constructor($){super($);this.name="TokenUnresolvedError"}};y6=class y6 extends Error{static errorKind="stale_auth";errorKind="stale_auth";constructor($){super($);this.name="StaleAuthError"}};f5=class f5 extends Error{static errorKind="grant_refused";errorKind="grant_refused";constructor($){super($);this.name="GrantRefusedError"}};u5=class u5 extends Error{static errorKind="identity_unavailable";errorKind="identity_unavailable";retryAfter;detail;constructor($="",Z=5){super($||"identity lookup unavailable");this.name="IdentityUnavailableError",this.detail=$,this.retryAfter=Z}};Fw=new Set(["\t",` -`,"\v","\f","\r"," ","…"," "]);nz=E0([O0("principal",Z$(),!1),O0("token_name",Z$(),!1),O0("ttl_seconds",VJ(),!1)]),oz=E0([O0("token",Z$(),!1),O0("expires_at",tY(),!1),O0("grant_id",Z$(),!1)])});function Uw(){let $=new Uint8Array(16);crypto.getRandomValues($);let Z="";for(let Q=0;Q<$.length;Q++)Z+=$[Q].toString(16).padStart(2,"0");return Z}class c1{protocol;serverId;protocolVersion;dispatchHook=null;externalConfig;onServeStart=null;serveStartFired=!1;extraBindings=new Map;constructor($,Z){if(this.protocol=$,this.serverId=Z?.serverId??crypto.randomUUID().replace(/-/g,"").slice(0,12),this.dispatchHook=Z?.dispatchHook??null,this.externalConfig=Z?.externalLocation,this.protocolVersion=Z?.protocolVersion??"",this.onServeStart=Z?.onServeStart??null,Z?.enableDescribe??!0)this.registerReflection()}bindings(){let $=new Map;$.set(this.protocol.name,{name:this.protocol.name,protocol:this.protocol,versionExempt:!1});for(let[Z,Q]of this.extraBindings)$.set(Z,Q);return $}registerReflection(){if(this.extraBindings.has(uZ))return;let $=z5({listBindings:()=>this.bindings(),hashFor:(Z)=>{let Q=this.bindings().get(Z);return Q?cW(Q):Promise.resolve("")},serverId:()=>this.serverId,serverVersion:()=>""});this.addProtocol({name:uZ,protocol:$,versionExempt:!0},!0)}registerIdentity($){let Z=c5($);if(!Z)return;this.addProtocol({name:f1,protocol:Z,versionExempt:!1},!0)}addProtocol($,Z=!1){if(xW($.name,Z),$.name===this.protocol.name||this.extraBindings.has($.name))throw Error(`Two protocols are hosted under the same name '${$.name}'. The name is the routing key, so it must be unique.`);this.extraBindings.set($.name,$)}resolve($,Z){if(Z===pW)throw new XX(U1());let Q=this.bindings(),X=[...Q.keys()].sort();if(!$)throw new vW(X);try{xW($,!0)}catch(Y){throw new T8(`'vgi_rpc.protocol' is not a protocol name: ${Y.message}`)}let J=Q.get($);if(!J)throw T8.notHosted($,X);let W=J.protocol.getMethod(Z);if(!W){let Y=J.protocol.methodNames().sort();throw new XX(`Protocol '${$}' has no method '${Z}'. Available: [${Y.join(", ")}].`)}return{method:W,binding:J}}async notifyTransport($){if(this.serveStartFired)return;if(this.onServeStart)await this.onServeStart($);this.serveStartFired=!0}checkProtocolVersion($,Z){let Q=Z?.protocol??this.protocol,X=Q.protocolVersionParts,J=Q.protocolVersion;if($===void 0)throw new xX(`VGI client/worker protocol_version mismatch. - Client: - Server: ${J} - Direction: the client did not send a vgi_rpc.protocol_version metadata key. This is either a vgi-rpc framework bug or a non-VGI client connecting to a VGI worker.`);let W;try{W=VW($)}catch{throw new xX(`VGI client/worker protocol_version mismatch. - Client: ${$} - Server: ${J} - Direction: client sent a malformed protocol_version. Expected canonical semver MAJOR.MINOR.PATCH.`)}if(W[0]===X[0]&&W[1]===X[1])return;let G=W[0]{qZ();z1();dL();oL();HZ();M1();az();lX();q5();S1();mX();B7();mJ=E0([])});async function rL($,Z){await new c1($,Z.serverOptions).serveConnection(Z.readable,Z.writable,Z.transportKind)}var aL=O(()=>{tz()});async function tL($,Z){let Q=kQ($);if(Z)Q=await E$(Q,Z);let X=yJ(Q.schema,Q);return{methodName:X.methodName,params:X.params,schema:Q.schema}}function eL($,Z,Q,X){return vQ(Z,Q,$,{protocolVersion:X})}function $A($,Z,Q="",X=null){let J=Z??E0([]);return f0(J,[q$(J,$,Q,X)])}function ZA($){return XA($,y$)}function QA($){return XA($,SJ)}async function XA($,Z){try{let Q=await L1($);while(!0){let X=await Q.readStream();if(!X)return null;for(let J of X.batches){let W=J.metadata?.get(Z);if(W)return W}}}catch{return null}}async function JA($){let Q=await(await L1($)).readStream();if(!Q)return null;for(let X of Q.batches){if(X.numRows>0){let J=X.getChild("result");if(!J)return null;let W=J.get(0);if(W==null)return null;return{envelopeSchema:X.schema,resultBytes:W}}if(!X.metadata?.has(KQ))return null}return null}function WA($,Z){let Q=$.fields.length===0?GQ($,new Map):vX($,{result:Z},new Map);return f0($,[Q])}var YA=O(()=>{qZ();S6();I8();J8();S1();mX()});var GA=O(()=>{ZN();q6();HZ();I8();uL();mW();cL();X5();X7();aL();tz();az();lX();YA();EU()});var KA={};P8(KA,{writeUnaryResult:()=>WA,writeRequest:()=>eL,validateSpiffeId:()=>WY,utf8Length:()=>lz,unpackStateToken:()=>x1,uint8:()=>xN,uint64:()=>hN,uint32:()=>yN,uint16:()=>vN,trimForShapeTest:()=>p5,tokenDigest:()=>iL,toSchema:()=>TQ,subprocessConnect:()=>VU,str:()=>BN,spiffeX509HeaderProvider:()=>Bz,serveStream:()=>rL,rpcPathFromPrefix:()=>_8,rpcPath:()=>K1,resolveExternalLocation:()=>E$,reservedPath:()=>j6,requirePeerIdentity:()=>lN,rejectJwsShaped:()=>cz,redactClaims:()=>zH,readUnaryResult:()=>JA,readRequest:()=>tL,pipeConnect:()=>k7,pickApplicationProtocol:()=>D1,peerIdentityPrimary:()=>bW,parseXfcc:()=>y5,parseUseIdTokenAsBearer:()=>NU,parseResourceMetadataUrl:()=>qU,parseDeviceCodeClientSecret:()=>MU,parseDeviceCodeClientId:()=>UU,parseClientSecret:()=>FU,parseClientId:()=>PU,parseCapabilitiesFromHeaders:()=>fG,otelTraceContext:()=>qH,observePeerIdentity:()=>Z5,oauthResourceMetadataToJson:()=>V1,normalisePrincipals:()=>fz,noRedaction:()=>eF,nginxSpiffeProvider:()=>kz,mtlsAuthenticateXfcc:()=>jz,mtlsAuthenticateSubject:()=>Rz,mtlsAuthenticateFingerprint:()=>Vz,mtlsAuthenticate:()=>h1,maybeExternalizeBatch:()=>kJ,makeExternalLocationBatch:()=>G1,jwtAuthenticate:()=>Cz,jsonStateSerializer:()=>y1,isJwsShaped:()=>pz,isExternalLocationBatch:()=>F$,isCapabilitySnapshotFresh:()=>uG,irohHttpBridgeOptions:()=>pL,irohForwardedHeaderIdentityProvider:()=>yz,int8:()=>gN,int32:()=>_N,int16:()=>TN,int:()=>IN,inferParamTypes:()=>yW,httpsOnlyValidator:()=>sG,httpOAuthMetadata:()=>zU,httpIntrospect:()=>O1,httpConnect:()=>V7,headersFromNodeRawHeaders:()=>wz,gcpLoadBalancerSpiffeProvider:()=>Tz,float32:()=>mN,float:()=>bN,findStateToken:()=>ZA,findProtocolVersion:()=>QA,fetchOAuthMetadata:()=>E7,envoyXfccSpiffeProvider:()=>gz,encodeTokenIdentity:()=>sz,encodeIssuedGrant:()=>iz,discoverHttpCapabilities:()=>k8,decodeContentEncoding:()=>tH,createHttpHandler:()=>Kz,checkIntrospector:()=>uz,checkFreshness:()=>dz,chainAuthenticate:()=>E1,bytes:()=>kN,buildIdentityProtocol:()=>c5,buildErrorStream:()=>$A,bool:()=>fN,bearerAuthenticateStatic:()=>x7,bearerAuthenticate:()=>L5,azureApplicationGatewaySpiffeProvider:()=>Iz,awsAlbSpiffeProvider:()=>_z,anyOfPeerIdentities:()=>dN,allOfPeerIdentities:()=>nN,adaptServiceDescription:()=>A1,VgiRpcServer:()=>c1,VersionError:()=>gX,UPLOAD_URL_RESPONSE_SCHEMA:()=>TJ,UPLOAD_URL_PARAMS_SCHEMA:()=>V6,UPLOAD_URL_METHOD:()=>_Q,TransportKind:()=>gQ,TokenUnresolvedError:()=>p1,TOKEN_IDENTITY_SCHEMA:()=>nz,SubjectStability:()=>W8,StaleAuthError:()=>y6,SessionLostError:()=>B8,ServerDrainingError:()=>sY,STATE_KEY:()=>y$,SERVER_SCOPE:()=>rW,SERVER_ID_KEY:()=>wW,RpcError:()=>b,RPC_METHOD_KEY:()=>yX,RPC_ERROR_HEADER:()=>BJ,REQUEST_VERSION_KEY:()=>hX,REQUEST_VERSION:()=>IQ,REQUEST_ID_KEY:()=>wJ,REDACTED:()=>HH,Protocol:()=>E6,PipeStreamSession:()=>j1,PeerSubjectKind:()=>HX,PeerResolutionContext:()=>P1,PeerIdentityUnavailableError:()=>BZ,PeerIdentityStatus:()=>C0,PeerIdentityResult:()=>P$,PeerIdentityRejectedError:()=>A$,PeerIdentity:()=>pX,PeerEvidenceSet:()=>cX,OutputCollector:()=>xQ,MethodType:()=>S$,MethodNotImplementedError:()=>XX,MAX_UPLOAD_URL_COUNT:()=>IW,MAX_TOKEN_BYTES:()=>hz,LOG_MESSAGE_KEY:()=>M6,LOG_LEVEL_KEY:()=>KQ,LOG_EXTRA_KEY:()=>EJ,IntrospectionRefusedError:()=>u1,IdentityUnavailableError:()=>u5,IdentityImpl:()=>rz,IdentityAssurance:()=>KX,ISSUED_GRANT_SCHEMA:()=>oz,IROH_FORWARDED_ENDPOINT_HEADER:()=>vz,IDENTITY_PROTOCOL_NAME:()=>f1,HttpStreamSession:()=>B6,GrantRefusedError:()=>f5,FdSink:()=>KH,ERROR_KIND_SESSION_LOST:()=>vG,ERROR_KIND_SERVER_DRAINING:()=>yG,ERROR_KIND_METHOD_NOT_IMPLEMENTED:()=>xG,ERROR_KIND_KEY:()=>dG,DEFAULT_MAX_AUTH_AGE_SECONDS:()=>mz,DEFAULT_IDENTITY_TTL_SECONDS:()=>bz,AuthUnavailableError:()=>qX,AuthReason:()=>CZ,AuthFailure:()=>qQ,AuthContext:()=>zZ,AccessLogSampler:()=>gG,AccessLogHook:()=>PH,AUTH_REASON_HEADER:()=>nW,AUTH_PROXY_REQUIRED_HEADER:()=>oW,ARROW_CONTENT_TYPE:()=>OZ});var ez=O(()=>{GA()});JQ();JQ();JQ();JQ();JQ();JQ();JQ();JQ();ez();var Mw=Object.defineProperty,Lw=($)=>$;function Aw($,Z){this[$]=Lw.bind(null,Z)}var Dw=($,Z)=>{for(var Q in Z)Mw($,Q,{get:Z[Q],enumerable:!0,configurable:!0,set:Aw.bind(Z,Q)})},Q$={Null:1,Int:2,Float:3,Binary:4,Utf8:5,Bool:6,Decimal:7,Date:8,Time:9,Timestamp:10,Interval:11,List:12,Struct:13,Union:14,FixedSizeBinary:15,FixedSizeList:16,Map:17,Duration:18,LargeBinary:19,LargeUtf8:20,Dictionary:-1};function Q9($){return $.typeId===Q$.Int}function Ow($){return $.typeId===Q$.Binary||$.typeId===Q$.LargeBinary}function Cw($){return $.typeId===Q$.Bool}function jw($){return $.typeId===Q$.List}function Vw($){return $.typeId===Q$.Struct}function Rw($){return $.typeId===Q$.Map}function Ew($){return $.typeId===Q$.Dictionary}var OA={};Dw(OA,{DateUnit:()=>ww,IntervalUnit:()=>Sw,TimeUnit:()=>d1,UnionMode:()=>Bw,adoptArrowJsShape:()=>iS,backend:()=>sS,batchFromColumns:()=>lJ,batchFromRows:()=>fJ,batchToScalarDict:()=>bA,batchToSecretDict:()=>fS,binary:()=>kA,bool:()=>jA,buildStatisticsBatch:()=>oS,columnFromArray:()=>bS,date:()=>G9,dateDay:()=>hw,dateMillisecond:()=>bw,decimal:()=>Y9,decimal128:()=>vw,decimal256:()=>yw,decodeDictValue:()=>mS,denseUnion:()=>ow,deserializeBatch:()=>GY,deserializeSchema:()=>a5,dictionary:()=>xA,duration:()=>cw,emptyBatch:()=>r5,field:()=>cJ,filterBatch:()=>cS,fixedSizeBinary:()=>xw,fixedSizeList:()=>dw,float16:()=>_w,float32:()=>Tw,float64:()=>SA,int:()=>Iw,int16:()=>i5,int32:()=>X9,int64:()=>J9,int8:()=>VA,interval:()=>lw,iterRows:()=>y8,largeBinary:()=>IA,largeUtf8:()=>gw,list:()=>TA,map:()=>gA,nullType:()=>CA,projectBatch:()=>lS,projectSchema:()=>uA,readCanonicalValue:()=>m8,safeNumber:()=>pS,schema:()=>kw,serializeBatch:()=>MX,serializeSchema:()=>o5,sparseUnion:()=>nw,struct:()=>K9,time:()=>s1,timeMicrosecond:()=>uw,timeMillisecond:()=>fw,timeNanosecond:()=>pw,timeSecond:()=>mw,timestamp:()=>_A,uint16:()=>EA,uint32:()=>wA,uint64:()=>W9,uint8:()=>RA,union:()=>H9,utf8:()=>BA,withBatchMetadata:()=>dS});var d1={SECOND:l.SECOND,MILLISECOND:l.MILLISECOND,MICROSECOND:l.MICROSECOND,NANOSECOND:l.NANOSECOND},ww={DAY:r0.DAY,MILLISECOND:r0.MILLISECOND},Sw={YEAR_MONTH:s0.YEAR_MONTH,DAY_TIME:s0.DAY_TIME,MONTH_DAY_NANO:s0.MONTH_DAY_NANO},Bw={Sparse:m0.Sparse,Dense:m0.Dense};function kw($,Z){return new Q0($,Z??new Map)}function cJ($,Z,Q=!0,X){return new a($,Z,Q,X??new Map)}var CA=()=>new j$,jA=()=>new V$,VA=()=>new s8,i5=()=>new i8,X9=()=>new GZ,J9=()=>new AZ,RA=()=>new r8,EA=()=>new a8,wA=()=>new t8,W9=()=>new VQ;function Iw($=32,Z=!0){if(Z)switch($){case 8:return VA();case 16:return i5();case 32:return X9();case 64:return J9()}else switch($){case 8:return RA();case 16:return EA();case 32:return wA();case 64:return W9()}throw Error(`int: unsupported bitWidth ${$}`)}var _w=()=>new t6,Tw=()=>new e8,SA=()=>new $Q,BA=()=>new T$,gw=()=>new aQ,kA=()=>new xZ,IA=()=>new rQ,xw=($)=>new eQ($);function Y9($,Z,Q=128){return new tQ(Z,$,Q)}var vw=($,Z)=>Y9($,Z,128),yw=($,Z)=>Y9($,Z,256),G9=($=r0.MILLISECOND)=>new O8($),hw=()=>G9(r0.DAY),bw=()=>G9(r0.MILLISECOND);function s1($=l.MILLISECOND,Z){let Q=Z??($<=l.MILLISECOND?32:64);return new C8($,Q)}var mw=()=>s1(l.SECOND,32),fw=()=>s1(l.MILLISECOND,32),uw=()=>s1(l.MICROSECOND,64),pw=()=>s1(l.NANOSECOND,64),_A=($=l.MILLISECOND,Z=null)=>new tZ($,Z),cw=($=l.MILLISECOND)=>new V8($),lw=($=s0.MONTH_DAY_NANO)=>new j8($);function TA($){let Z=o1($)?$:cJ("item",$,!0);return new LZ(Z)}function dw($,Z){let Q=o1($)?$:cJ("item",$,!0);return new jQ(Z,Q)}function K9($){return new j0($)}function gA($,Z,Q=!1){let X=o1($)?$:cJ("key",$,!1),J=o1(Z)?Z:cJ("value",Z,!0),W=cJ("entries",K9([X,J]),!1);return new eZ(W,Q)}function xA($,Z=i5(),Q=!1,X){return new YZ($,Z,X,Q)}function H9($,Z,Q){let X=Z.map((Y,G)=>o1(Y)?Y:cJ(String(G),Y,!0)),J=Q??X.map((Y,G)=>G);return new($===m0.Dense?EY:HJ)(J,X)}var nw=($,Z)=>H9(m0.Sparse,$,Z),ow=($,Z)=>H9(m0.Dense,$,Z);function o1($){return typeof $.name==="string"&&$.type!=null}function n5($){let Z=$.type;if(k.isList(Z)){let Q=Z.children[0];return I({type:Z,length:0,nullCount:0,child:n5(Q),valueOffsets:new Int32Array([0])})}if(k.isMap(Z)){let Q=Z.children[0];return I({type:Z,length:0,nullCount:0,child:n5(Q),valueOffsets:new Int32Array([0])})}if(k.isStruct(Z)){let Q=Z.children.map((X)=>n5(X));return I({type:Z,length:0,nullCount:0,children:Q})}return I({type:Z,length:0,nullCount:0})}function r5($){let Z=$,Q=Z.fields.map((W)=>n5(W)),X=new j0(Z.fields),J=I({type:X,length:0,children:Q,nullCount:0});return new D0(Z,J)}var l1=86400000;function a0($){return $===null||$===void 0}function _0($,Z,Q){let X;try{X=typeof Q==="bigint"?`${Q}n`:typeof Q==="object"?Object.prototype.toString.call(Q):String(Q)}catch{X=""}throw TypeError(`codec[${$}]: ${Z} (got ${typeof Q}: ${X})`)}function bQ($,Z){let Q=(X)=>{if(a0(X))return null;return Z(X),X};return{label:$,richToCanonical:Q,canonicalToRich:Q,rawToCanonical:Q,canonicalToRaw:Q}}function sw($){return(Z)=>{if(typeof Z!=="boolean")_0($,"expected boolean",Z)}}function KY($,Z,Q){return(X)=>{let J;if(typeof X==="number")J=X;else if(typeof X==="bigint"){if(XBigInt(Q))_0($,`bigint out of ${$} range`,X);return}else return _0($,"expected number",X);if(!Number.isInteger(J))_0($,"expected an integer",X);if(JQ)_0($,`value out of ${$} range [${Z}, ${Q}]`,X)}}function iw($){return(Z)=>{if(typeof Z==="number")return;if(typeof Z==="bigint")return;_0($,"expected number",Z)}}function vA($,Z,Q){let X=Q?(1n<{let Y;if(typeof W==="bigint")Y=W;else if(typeof W==="number"){if(!Number.isInteger(W))_0($,"expected an integer",W);Y=BigInt(W)}else return _0($,"expected bigint",W);if(YX)_0($,`value out of ${$} range`,W)}}function HA($){return(Z)=>{if(typeof Z!=="string")_0($,"expected string",Z)}}function n1($,Z){if(typeof Z==="bigint")return Z;if(typeof Z==="number"){if(!Number.isInteger(Z))_0($,"expected an integer value",Z);return BigInt(Z)}return _0($,"expected number or bigint",Z)}function b6($,Z){if(Z<-9007199254740991n||Z>9007199254740991n)_0($,"bigint overflows the safe-integer range when narrowing to number",Z);return Number(Z)}function rw(){return{label:"date32",richToCanonical(Z){if(a0(Z))return null;if(Z instanceof Date){let Q=Z.getTime();if(Number.isNaN(Q))_0("date32","invalid Date",Z);if(Q%l1!==0)return Math.floor(Q/l1);return Q/l1}if(typeof Z==="number"){if(!Number.isInteger(Z))_0("date32","day-number must be an integer",Z);return Z}if(typeof Z==="bigint")return b6("date32",Z);return _0("date32","expected Date, number (days), or bigint",Z)},canonicalToRich(Z){if(a0(Z))return null;let Q;if(typeof Z==="number"){if(!Number.isInteger(Z))_0("date32","canonical day-number must be an integer",Z);Q=Z}else if(typeof Z==="bigint")Q=b6("date32",Z);else if(Z instanceof Date)return Z;else return _0("date32","expected number (days) canonical",Z);let X=Q*l1,J=new Date(X);if(Number.isNaN(J.getTime()))_0("date32","day-number produces an invalid Date",Z);return J},rawToCanonical(Z){if(a0(Z))return null;if(typeof Z==="number"){if(!Number.isInteger(Z))_0("date32","raw day-number must be an integer",Z);return Z}if(typeof Z==="bigint")return b6("date32",Z);return _0("date32","expected branded Date32 (day-number)",Z)},canonicalToRaw(Z){if(a0(Z))return null;if(typeof Z==="number"){if(!Number.isInteger(Z))_0("date32","canonical day-number must be an integer",Z);return Z}if(typeof Z==="bigint")return b6("date32",Z);if(Z instanceof Date){let Q=Z.getTime();if(Number.isNaN(Q))_0("date32","invalid Date",Z);return Math.floor(Q/l1)}return _0("date32","expected number (days) canonical",Z)}}}function aw(){return{label:"date64",richToCanonical(Z){if(a0(Z))return null;if(Z instanceof Date){let Q=Z.getTime();if(Number.isNaN(Q))_0("date64","invalid Date",Z);return BigInt(Q)}if(typeof Z==="bigint")return Z;if(typeof Z==="number"){if(!Number.isInteger(Z))_0("date64","ms value must be an integer",Z);return BigInt(Z)}return _0("date64","expected Date, bigint (ms), or number",Z)},canonicalToRich(Z){if(a0(Z))return null;if(Z instanceof Date)return Z;let Q=n1("date64",Z),X=b6("date64",Q),J=new Date(X);if(Number.isNaN(J.getTime()))_0("date64","ms value produces an invalid Date",Z);return J},rawToCanonical(Z){if(a0(Z))return null;if(Z instanceof Date)return _0("date64","raw Date64Ms must be a bigint (ms), not a Date",Z);return n1("date64",Z)},canonicalToRaw(Z){if(a0(Z))return null;if(Z instanceof Date)return BigInt(Z.getTime());return n1("date64",Z)}}}function tw(){let Z=(X)=>{if(a0(X))return null;if(typeof X==="number"){if(!Number.isInteger(X))_0("time32","time32 value must be an integer",X);return X}if(typeof X==="bigint")return b6("time32",X);return _0("time32","expected number (raw time unit)",X)},Q=(X)=>{if(a0(X))return null;if(typeof X==="number")return X;if(typeof X==="bigint")return b6("time32",X);return _0("time32","expected number canonical",X)};return{label:"time32",richToCanonical:Z,canonicalToRich:Q,rawToCanonical:Z,canonicalToRaw:Q}}function $9($){let Z=(Q)=>{if(a0(Q))return null;return n1($,Q)};return{label:$,richToCanonical:Z,canonicalToRich:Z,rawToCanonical:Z,canonicalToRaw:Z}}function ew($){let Z=new Uint8Array($.buffer,$.byteOffset,$.byteLength),Q=0n;for(let X=Z.length-1;X>=0;X--)Q=Q<<8n|BigInt(Z[X]);if(Z.length>0&&Z[Z.length-1]&128)Q-=1n<{if(a0(X))return null;if(typeof X==="bigint")return X;if(typeof X==="number")return n1("decimal",X);if(ArrayBuffer.isView(X))return ew(X);return _0("decimal","expected bigint, number, or decimal byte view",X)},Q=(X)=>{if(a0(X))return null;return Z(X)};return{label:"decimal",richToCanonical:Z,canonicalToRich:Q,rawToCanonical:Z,canonicalToRaw:Q}}function ZS($){let Q=$.children.map((J)=>({name:J.name,codec:b8(J.type)})),X="struct";return{label:"struct",richToCanonical(J){if(a0(J))return null;if(typeof J!=="object")_0("struct","expected an object",J);let W=J,Y={};for(let{name:G,codec:H}of Q)Y[G]=H.richToCanonical(W[G]??null);return Y},canonicalToRich(J){if(a0(J))return null;if(typeof J!=="object")_0("struct","expected an object",J);let W=J,Y={};for(let{name:G,codec:H}of Q)Y[G]=H.canonicalToRich(W[G]??null);return Y},rawToCanonical(J){if(a0(J))return null;if(typeof J!=="object")_0("struct","expected an object",J);let W=J,Y={};for(let{name:G,codec:H}of Q)Y[G]=H.rawToCanonical(W[G]??null);return Y},canonicalToRaw(J){if(a0(J))return null;if(typeof J!=="object")_0("struct","expected an object",J);let W=J,Y={};for(let{name:G,codec:H}of Q)Y[G]=H.canonicalToRaw(W[G]??null);return Y}}}function QS($){let Z=$.children?.[0];if(!Z)throw Error("codec[list]: list type has no child field");let Q=b8(Z.type),X="list",J=(W)=>{if(Array.isArray(W))return W;if(W!=null&&typeof W[Symbol.iterator]==="function")return Array.from(W);return _0(X,"expected an array or iterable",W)};return{label:X,richToCanonical(W){if(a0(W))return null;return J(W).map((Y)=>Q.richToCanonical(Y))},canonicalToRich(W){if(a0(W))return null;return J(W).map((Y)=>Q.canonicalToRich(Y))},rawToCanonical(W){if(a0(W))return null;return J(W).map((Y)=>Q.rawToCanonical(Y))},canonicalToRaw(W){if(a0(W))return null;return J(W).map((Y)=>Q.canonicalToRaw(Y))}}}function XS($){let Q=$.children?.[0]?.type?.children;if(!Q||Q.length<2)throw Error("codec[map]: map type has no key/value entry fields");let X=b8(Q[0].type),J=b8(Q[1].type),W="map",Y=(G)=>{if(Array.isArray(G))return G;if(G instanceof Map)return Array.from(G.entries());if(G!=null&&typeof G[Symbol.iterator]==="function")return Array.from(G);if(G!=null&&typeof G==="object")return Object.entries(G);return _0(W,"expected Map, array of pairs, or object",G)};return{label:W,richToCanonical(G){if(a0(G))return null;return Y(G).map(([H,z])=>[X.richToCanonical(H),J.richToCanonical(z)])},canonicalToRich(G){if(a0(G))return null;return Y(G).map(([H,z])=>[X.canonicalToRich(H),J.canonicalToRich(z)])},rawToCanonical(G){if(a0(G))return null;return Y(G).map(([H,z])=>[X.rawToCanonical(H),J.rawToCanonical(z)])},canonicalToRaw(G){if(a0(G))return null;return Y(G).map(([H,z])=>[X.canonicalToRaw(H),J.canonicalToRaw(z)])}}}function JS($){let Z=$.dictionary;if(!Z)throw Error("codec[dictionary]: missing dictionary value type");let Q=b8(Z);return{label:`dictionary<${Q.label}>`,richToCanonical:(X)=>Q.richToCanonical(X),canonicalToRich:(X)=>Q.canonicalToRich(X),rawToCanonical:(X)=>Q.rawToCanonical(X),canonicalToRaw:(X)=>Q.canonicalToRaw(X)}}var WS=bQ("int8",KY("int8",-128,127)),YS=bQ("int16",KY("int16",-32768,32767)),GS=bQ("int32",KY("int32",-2147483648,2147483647)),KS=bQ("uint8",KY("uint8",0,255)),HS=bQ("uint16",KY("uint16",0,65535)),zS=bQ("uint32",KY("uint32",0,4294967295)),qS=bQ("int64",vA("int64",64,!0)),PS=bQ("uint64",vA("uint64",64,!1)),FS=bQ("float",iw("float")),NS=bQ("bool",sw("bool")),US={label:"utf8",richToCanonical($){if(a0($))return null;return typeof $==="string"?$:String($)},canonicalToRich($){if(a0($))return null;return HA("utf8")($),$},rawToCanonical($){if(a0($))return null;return typeof $==="string"?$:String($)},canonicalToRaw($){if(a0($))return null;return HA("utf8")($),$}};function MS($,Z){return(Q)=>{if(a0(Q))return null;let X;if(Q instanceof Uint8Array)X=Q;else if(ArrayBuffer.isView(Q)){let J=Q;X=new Uint8Array(J.buffer,J.byteOffset,J.byteLength)}else return _0($,"expected Uint8Array",Q);if(Z!=null&&X.length!==Z)_0($,`expected exactly ${Z} bytes`,Q);return X}}function yA($,Z){let Q=MS($,Z);return{label:$,richToCanonical:Q,canonicalToRich:Q,rawToCanonical:Q,canonicalToRaw:Q}}var LS=yA("binary"),AS={label:"null",richToCanonical:()=>null,canonicalToRich:()=>null,rawToCanonical:()=>null,canonicalToRaw:()=>null};function b8($){switch($.typeId){case Q$.Null:return AS;case Q$.Bool:return NS;case Q$.Int:{let Z=$.bitWidth??32,Q=$.isSigned??$.signed??!0;if(Z===64)return Q?qS:PS;if(Q){if(Z===8)return WS;if(Z===16)return YS;return GS}if(Z===8)return KS;if(Z===16)return HS;return zS}case Q$.Float:return FS;case Q$.Utf8:case Q$.LargeUtf8:return US;case Q$.Binary:case Q$.LargeBinary:return LS;case Q$.FixedSizeBinary:return yA("fixedSizeBinary",$.byteWidth??$.stride);case Q$.Decimal:return $S();case Q$.Date:return $.unit===0?rw():aw();case Q$.Time:return($.bitWidth??32)===64?$9("time64"):tw();case Q$.Timestamp:return $9("timestamp");case Q$.Duration:return $9("duration");case Q$.Struct:return ZS($);case Q$.Union:return bQ("union",()=>{});case Q$.List:case Q$.FixedSizeList:return QS($);case Q$.Map:return XS($);case Q$.Dictionary:return JS($);case Q$.Interval:return bQ("interval",()=>{});default:return bQ(`type#${$.typeId}`,()=>{})}}function hA($,Z){return YY(Z,$)}function YY($,Z){if(k.isList(Z))return RS($,Z);if(k.isFixedSizeList(Z))return ES($,Z);if(k.isMap(Z))return wS($,Z);if(k.isStruct(Z))return SS($,Z);if(k.isDecimal(Z))return CS($,Z);if(k.isDate(Z)||k.isTime(Z)||k.isTimestamp(Z)||k.isDuration(Z)){if(DS(Z)===64)return zA($,Z);return OS($,Z)}if(k.isInt(Z)&&Z.bitWidth===64)return zA($,Z);return QX($,Z).data[0]}function DS($){if(k.isDate($))return $.unit===r0.DAY?32:64;if(k.isTime($))return $.bitWidth??32;return 64}function OS($,Z){let Q=$.length,X=new Int32Array(Q),J=new Uint8Array(Math.ceil(Q/8)),W=0;for(let Y=0;Y>3]|=1<<(Y&7),X[Y]=typeof G==="bigint"?Number(G):G}return I({type:Z,length:Q,nullBitmap:W>0?J:void 0,data:X,nullCount:W})}function zA($,Z){let Q=$.length,X=new BigInt64Array(Q),J=new Uint8Array(Math.ceil(Q/8)),W=0;for(let G=0;G>3]|=1<<(G&7),X[G]=typeof H==="bigint"?H:BigInt(H)}let Y=new Int32Array(X.buffer);return I({type:Z,length:Q,nullBitmap:W>0?J:void 0,data:Y,nullCount:W})}function CS($,Z){let Q=Z.byteWidth??(Z.bitWidth?Z.bitWidth/8:16),X=$.length,J=new Uint8Array(X*Q),W=new Uint8Array(Math.ceil(X/8)),Y=0;for(let G=0;G>3]|=1<<(G&7);let z;if(H instanceof Uint8Array)z=H;else if(ArrayBuffer.isView(H)){let q=H;z=new Uint8Array(q.buffer,q.byteOffset,q.byteLength)}else z=VS(jS(H),Q);J.set(z.subarray(0,Q),G*Q)}return I({type:Z,length:X,nullBitmap:Y>0?W:void 0,data:new Uint32Array(J.buffer,J.byteOffset,J.byteLength/4),nullCount:Y})}function jS($){return typeof $==="bigint"?$:BigInt(Math.trunc(Number($)))}function VS($,Z){let Q=new Uint8Array(Z),X=$;if(X<0n)X=(1n<>=8n;return Q}function RS($,Z){let Q=Z.children[0],X=$.length,J=new Int32Array(X+1),W=[],Y=new Uint8Array(Math.ceil(X/8)),G=0;for(let z=0;z>3]|=1<<(z&7);let P=Array.isArray(q)?q:typeof q[Symbol.iterator]==="function"?Array.from(q):null;if(P)for(let U of P)W.push(U)}}J[X]=W.length;let H=W.length>0?YY(W,Q.type):pJ(Q.type);return I({type:Z,length:X,nullCount:G,nullBitmap:G>0?Y:void 0,child:H,offset:0,valueOffsets:J})}function ES($,Z){let Q=Z.children[0],X=Z.listSize,J=$.length,W=[],Y=new Uint8Array(Math.ceil(J/8)),G=0;for(let z=0;z>3]|=1<<(z&7);let P=Array.isArray(q)?q:Array.from(q);if(P.length!==X)throw TypeError(`fixed_size_list[${X}]: row ${z} has ${P.length} items`);for(let U of P)W.push(U)}let H=W.length>0?YY(W,Q.type):pJ(Q.type);return I({type:Z,length:J,nullCount:G,nullBitmap:G>0?Y:void 0,child:H})}function wS($,Z){let Q=$.length,X=Z.children[0],J=X.type.children[0],W=X.type.children[1],Y=new Int32Array(Q+1),G=[],H=[],z=new Uint8Array(Math.ceil(Q/8)),q=0;for(let L=0;L>3]|=1<<(L&7);let R=Array.isArray(D)?D:D instanceof Map?Array.from(D.entries()):typeof D[Symbol.iterator]==="function"?Array.from(D):typeof D==="object"?Object.entries(D):[];for(let[V,g]of R)G.push(V),H.push(g)}}Y[Q]=G.length;let P=G.length>0?YY(G,J.type):pJ(J.type),U=H.length>0?YY(H,W.type):pJ(W.type),M=I({type:X.type,length:G.length,children:[P,U],nullCount:0});return I({type:Z,length:Q,nullCount:q,nullBitmap:q>0?z:void 0,child:M,offset:0,valueOffsets:Y})}function SS($,Z){let Q=Z.children,X=$.length,J=new Uint8Array(Math.ceil(X/8)),W=0,Y=Q.map(()=>[]);for(let H=0;H>3]|=1<<(H&7);for(let q=0;qYY(Y[z],H.type));return I({type:Z,length:X,nullCount:W,nullBitmap:W>0?J:void 0,children:G})}function pJ($){if(k.isList($))return I({type:$,length:0,nullCount:0,child:pJ($.children[0].type),valueOffsets:new Int32Array([0])});if(k.isMap($))return I({type:$,length:0,nullCount:0,child:pJ($.children[0].type),valueOffsets:new Int32Array([0])});if(k.isFixedSizeList($))return I({type:$,length:0,nullCount:0,child:pJ($.children[0].type)});if(k.isStruct($)){let Z=$.children.map((Q)=>pJ(Q.type));return I({type:$,length:0,nullCount:0,children:Z})}return I({type:$,length:0,nullCount:0})}function m8($,Z,Q){let X=Z,J=X?.data?.[0];return kS($,X,J,Q)}function BS($,Z,Q){if(typeof $?.isValid==="function")return $.isValid(Q);let X=Z?.nullBitmap;if(X&&X.length>0)return(X[Q>>3]>>(Q&7)&1)===1;return!0}function kS($,Z,Q,X){if(!BS(Z,Q,X))return null;switch($.typeId){case 1:return null;case 6:return Z.get(X);case 2:{let W=$.bitWidth??32,Y=Q?.values?.[X];if(W===64)return typeof Y==="bigint"?Y:BigInt(Y);return Number(Y)}case 3:return Number(Q?.values?.[X]);case 5:case 20:return String(Z.get(X));case 4:case 19:case 15:return IS(Z.get(X));case 7:return _S($,Q,X);case 8:{let W=Q?.values?.[X];if($.unit===r0.DAY)return Number(W);return typeof W==="bigint"?W:BigInt(W)}case 9:{let W=Q?.values?.[X];if(($.bitWidth??32)===64)return typeof W==="bigint"?W:BigInt(W);return Number(W)}case 10:case 18:{let W=Q?.values?.[X];return typeof W==="bigint"?W:BigInt(W)}case 13:return gS($,Z,X);case 14:return xS($,Z,Q,X);case 12:case 16:return vS($,Z,Q,X);case 17:return yS($,Z,Q,X);case-1:return hS($,Z,Q,X);default:return Z.get(X)}}function IS($){if($==null)return null;if($ instanceof Uint8Array)return $;if(ArrayBuffer.isView($)){let Z=$;return new Uint8Array(Z.buffer,Z.byteOffset,Z.byteLength)}return $}function _S($,Z,Q){let X=$.byteWidth??($.bitWidth?$.bitWidth/8:16),J=Z.values,W=X/4,Y=Q*W,G=new Uint8Array(X);for(let H=0;H>>0;G[H*4]=z&255,G[H*4+1]=z>>>8&255,G[H*4+2]=z>>>16&255,G[H*4+3]=z>>>24&255}return TS(G)}function TS($){let Z=0n;for(let Q=$.length-1;Q>=0;Q--)Z=Z<<8n|BigInt($[Q]);if($[$.length-1]&128)Z-=1n<0?0:-1,G=$.typeIdToChildIndex,H=G&&G[Y]!==void 0?G[Y]:Y,z=J[H];if(!z)return{tag:null,value:null};let q=Q?.valueOffsets,P=q?q[X]:X,U=typeof Z.getChildAt==="function"?Z.getChildAt(H):Z.children?.[H],M=U?m8(z.type,U,P):null;return{tag:z.name,value:M}}function vS($,Z,Q,X){let J=$.children[0],W=typeof Z.getChildAt==="function"?Z.getChildAt(0):Z.children?.[0],Y=Q?.valueOffsets,G,H;if(Y)G=Y[X],H=Y[X+1];else{let q=$.listSize??0;G=X*q,H=G+q}let z=[];for(let q=G;qW[J.name]??null);return lJ(X,Q)}function lJ($,Z,Q="rich"){let X=Z,J=X.fields.length>0?$[X.fields[0].name]?.length??0:0,W=X.fields.map((H)=>{let z=$[H.name];if(!z)return I({type:H.type,length:J,nullCount:J});let q=b8(H.type),P=Q==="raw"?q.rawToCanonical:q.richToCanonical,U=z.map((M)=>P.call(q,M));return hA(H.type,U)}),Y=new j0(X.fields),G=I({type:Y,length:J,children:W,nullCount:0});return new D0(X,G)}function*y8($,Z="rich"){let Q=$,X=Q.schema.fields.map((J)=>b8(J.type));for(let J=0;J0){if((X[Z>>3]>>(Z&7)&1)===0)return null}let J=Number(Q.values[Z]);return $.dictionary.get(J)}return $}function fS($){if(!$)return{};let Z=$;if(Z.numRows===0)return{};let Q={};for(let X of Z.schema.fields){let J=Z.getChild(X.name);if(J){let W=X.type,Y=b8(W).canonicalToRich(m8(W,J,0)),G=X.name,H;if(X.name.startsWith("secret_")){let z=X.metadata?.get?.("secret_type");if(z)G=z,H=X.metadata?.get?.("scope")??void 0}if(Y&&typeof Y==="object"&&!ArrayBuffer.isView(Y)){let z={...Y};if(G in Q)throw Error(`batchToSecretDict: duplicate secret_type '${G}' (scope=${H??"none"}). Use scoped secrets with distinct scopes to avoid collisions.`);if(Q[G]=z,H)Q[`${G}:${H}`]=z}else if(Y===null||Y===void 0);else Q[G]={}}}return Q}var mA=BigInt(Number.MAX_SAFE_INTEGER),uS=-mA;function pS($){if(typeof $==="bigint"){if($>mA||$ Number.MAX_SAFE_INTEGER). Keep it as a bigint.`);return Number($)}return $}function cS($,Z){let Q=$,X=Q.numRows,J=0;for(let G=0;Gq.canonicalToRich(m8(z,H,P)))}return lJ(Y,Q.schema)}function fA($,Z,Q){for(let X of Z){if(X<-1)throw Error(`${$}: unexpected negative projection ID ${X} (only -1 for row_id is allowed)`);if(X>=Q)console.warn(`${$}: projection ID ${X} is out of bounds (schema has ${Q} fields), ignoring`)}return Z.filter((X)=>X>=0&&XQ.fields[J]))}function lS($,Z){let Q=Z;if(!$)return Q;let X=fA("projectBatch",$,Q.schema.fields.length);if(X.length===0)return Q;let J=uA($,Q.schema),W=X.map((H)=>{return Q.getChildAt(H).data[0]}),Y=new j0(J.fields),G=I({type:Y,length:Q.numRows,children:W,nullCount:0});return new D0(J,G)}function dS($,Z){let Q=$;return new D0(Q.schema,Q.data,Z)}function o5($){let Z=new XQ;return Z.reset(void 0,$),Z.close(),Z.toUint8Array(!0)}function a5($){let Z=z$.from($),Q=[...Z];if(Q.length>0)return Q[0].schema;if(Z.schema)return Z.schema;throw Error("Cannot deserialize schema from empty IPC stream")}function MX($){let Z=$,Q=new XQ;return Q.reset(void 0,Z.schema),Q._writeRecordBatch(Z),Q.close(),Q.toUint8Array(!0)}function GY($){let Z=z$.from($),Q=[...Z];if(Q.length===0){let X=Z.schema??new Q0([]);return r5(X)}return Q[0]}function nS($){let Z=$;return[$.typeId,Z.unit??"",Z.bitWidth??"",Z.precision??"",Z.scale??"",Z.byteWidth??""].join(":")}function uJ($,Z){let Q=Z;if(Q.typeId===1)return I({type:Q,length:$.length,nullCount:$.length});let X=b8(Z),J=$.map((W)=>X.richToCanonical(W));return hA(Z,J)}function oS($){if($.length===0){let U=new a("0",new j$,!0),M=new HJ([0],[U]),L=I({type:M,length:0,typeIds:new Int8Array(0),children:[I({type:new j$,length:0,nullCount:0})]}),D=I({type:M,length:0,typeIds:new Int8Array(0),children:[I({type:new j$,length:0,nullCount:0})]});return qA([],L,D,[],[],[],[],[],M)}let Z=[],Q=new Map,X=[];for(let U of $){let M=nS(U.arrowType),L=Q.get(M);if(L===void 0)L=Z.length,Q.set(M,L),Z.push(U.arrowType);X.push(L)}let J=[],W=[],Y=[],G=[];for(let[U,M]of Z.entries()){J.push(new a(String(U),M,!0)),G.push(U);let L=$.map((R,V)=>X[V]===U?R.min:null),D=$.map((R,V)=>X[V]===U?R.max:null);W.push(uJ(L,M)),Y.push(uJ(D,M))}let H=new HJ(G,J),z=Int8Array.from(X),q=I({type:H,length:$.length,typeIds:z,children:W}),P=I({type:H,length:$.length,typeIds:z,children:Y});return qA($.map((U)=>U.columnName),q,P,$.map((U)=>U.hasNull),$.map((U)=>U.hasNotNull),$.map((U)=>U.distinctCount==null?null:BigInt(U.distinctCount)),$.map((U)=>U.containsUnicode),$.map((U)=>U.maxStringLength==null?null:BigInt(U.maxStringLength)),H)}function qA($,Z,Q,X,J,W,Y,G,H){let z=new Q0([new a("column_name",new T$,!1),new a("min",H,!0),new a("max",H,!0),new a("has_null",new V$,!1),new a("has_not_null",new V$,!1),new a("distinct_count",new AZ,!0),new a("contains_unicode",new V$,!0),new a("max_string_length",new VQ,!0)]),q=$.length,P=[uJ($,new T$),Z,Q,uJ(X,new V$),uJ(J,new V$),uJ(W,new AZ),uJ(Y,new V$),uJ(G,new VQ)],U=new j0(z.fields),M=I({type:U,length:q,children:P,nullCount:0});return new D0(z,M)}var sS={name:"arrow-js"};function iS($){return $}var{TimeUnit:rS,DateUnit:ud,IntervalUnit:pd,UnionMode:cd,schema:C}=OA;function K($,Z,Q,X){return cJ($,Z,Q,X)}var pA=()=>CA(),Z0=()=>jA(),Y$=()=>i5(),m6=()=>X9(),P0=()=>J9(),PA=()=>W9(),t5=()=>SA(),F=()=>BA(),N=()=>kA(),rX=()=>IA(),ld={[d1.SECOND]:"s",[d1.MILLISECOND]:"ms",[d1.MICROSECOND]:"us",[d1.NANOSECOND]:"ns"};function aS($,Z){return _A($,Z??null)}function j($){return TA($)}function h8($){return K9($)}function cZ($,Z,Q){return gA($,Z,Q)}function G$($,Z,Q,X){return xA($,Z,Q,X)}class h6{positional;named;argumentsSchema;constructor($=[],Z=new Map,Q=null){this.positional=$,this.named=Z,this.argumentsSchema=Q}get($,Z){if(typeof $==="number"){if($=Number.MIN_SAFE_INTEGER&&Z<=Number.MAX_SAFE_INTEGER)return Number(Z);return Z}return Z}return $}function tS($){let Z=[],Q={};for(let G=0;G<$.positional.length;G++){let H=$.positional[G],z=`positional_${G}`;Z.push(K(z,NA(H),!0)),Q[z]=H}for(let[G,H]of $.named){let z=`named_${G}`;Z.push(K(z,NA(H),!0)),Q[z]=H}let X=h8(Z),J=C([K("args",X,!0)]),W={};for(let G of Z){let H=Q[G.name];if(Q9(G.type)&&G.type.bitWidth===64&&typeof H==="number")H=BigInt(H);W[G.name]=H}let Y=lJ({args:[W]},J);return MX(Y)}function NA($){if($===null||$===void 0)return pA();if(typeof $==="string")return F();if(typeof $==="boolean")return Z0();if(typeof $==="number")return P0();if(typeof $==="bigint")return P0();if($ instanceof Uint8Array||$ instanceof ArrayBuffer)return N();return F()}var cA;(($)=>{$.SCALAR="scalar",$.TABLE="table",$.TABLE_BUFFERING="table_buffering",$.AGGREGATE="aggregate"})(cA||={});var eS;(($)=>{$.CONSISTENT="CONSISTENT",$.VOLATILE="VOLATILE",$.CONSISTENT_WITHIN_QUERY="CONSISTENT_WITHIN_QUERY"})(eS||={});var $B;(($)=>{$.DEFAULT="DEFAULT",$.SPECIAL="SPECIAL"})($B||={});var ZB;(($)=>{$.UNKNOWN="UNKNOWN",$.CONSTANT="CONSTANT",$.NON_DECREASING="NON_DECREASING",$.STRICTLY_INCREASING="STRICTLY_INCREASING",$.NON_INCREASING="NON_INCREASING",$.STRICTLY_DECREASING="STRICTLY_DECREASING"})(ZB||={});var QB;(($)=>{$.PRESERVES_ORDER="PRESERVES_ORDER",$.NO_ORDER_GUARANTEE="NO_ORDER_GUARANTEE",$.FIXED_ORDER="FIXED_ORDER"})(QB||={});var XB;(($)=>{$.ORDER_DEPENDENT="ORDER_DEPENDENT",$.NOT_ORDER_DEPENDENT="NOT_ORDER_DEPENDENT"})(XB||={});var JB;(($)=>{$.DISTINCT_DEPENDENT="DISTINCT_DEPENDENT",$.NOT_DISTINCT_DEPENDENT="NOT_DISTINCT_DEPENDENT"})(JB||={});var WB;(($)=>{$.INPUT="INPUT",$.FINALIZE="FINALIZE",$.TABLE_BUFFERING="TABLE_BUFFERING",$.TABLE_BUFFERING_FINALIZE="TABLE_BUFFERING_FINALIZE"})(WB||={});var YB="vgi_arg",GB="named",KB="vgi_type",HB="table",zB="any",qB="vgi_varargs",PB="true",FB="vgi_const",NB="true",UB="vgi_doc",MB="vgi_default",LB="vgi_choices",AB="vgi_range",DB="vgi_pattern";function K8($){if($ instanceof Uint8Array)return $;if($ instanceof ArrayBuffer)return new Uint8Array($);if(typeof Buffer<"u"&&Buffer.isBuffer($))return new Uint8Array($);if($&&$.buffer instanceof ArrayBuffer)return new Uint8Array($.buffer,$.byteOffset,$.byteLength);return new Uint8Array(0)}function s5($){if(typeof $==="string")return new TextEncoder().encode($);let Z=K8($);if(Z.length===0&&$!=null)throw Error(`Cannot convert ${typeof $} to Uint8Array`);return Z}function lA($,Z){let Q={};for(let X of $.fields){let J=Z[X.name];if(Q9(X.type)&&X.type.bitWidth===64&&typeof J==="number")J=BigInt(J);Q[X.name]=[J]}return lJ(Q,$)}var OB=h8([K("format",F(),!1),K("file_path",F(),!1),K("expected_schema",N(),!1)]),CB=h8([K("format",F(),!1),K("file_path",F(),!1)]),jB=C([K("function_name",F(),!1),K("arguments",N(),!1),K("function_type",G$(F(),Y$()),!1),K("input_schema",N(),!0),K("settings",N(),!0),K("secrets",N(),!0),K("attach_opaque_data",N(),!0),K("transaction_opaque_data",N(),!0),K("resolved_secrets_provided",Z0(),!1),K("at_unit",F(),!0),K("at_value",F(),!0),K("copy_from",OB,!0),K("copy_to",CB,!0),K("schema_path",j(K("item",F(),!0)),!0),K("argument_names",j(K("item",F(),!0)),!0)]);function dA($){return lA(jB,{function_name:$.function_name,arguments:tS($.arguments),function_type:$.function_type,input_schema:$.input_schema?o5($.input_schema):null,settings:$.settings?MX($.settings):null,secrets:$.secrets?MX($.secrets):null,attach_opaque_data:$.attach_opaque_data??null,transaction_opaque_data:$.transaction_opaque_data??null,resolved_secrets_provided:$.resolved_secrets_provided??!1,at_unit:$.at_unit??null,at_value:$.at_value??null,copy_from:$.copy_from?{format:$.copy_from.format,file_path:$.copy_from.file_path,expected_schema:o5($.copy_from.expected_schema)}:null,copy_to:$.copy_to?{format:$.copy_to.format,file_path:$.copy_to.file_path}:null,schema_path:$.schema_path??null,argument_names:$.argument_names??null})}function VB($){let Z=(Q)=>{if(!Q)return[];return(Array.isArray(Q)?Q:[...Q]).filter((J)=>J!=null).map(String)};return{output_schema:a5(s5($.output_schema)),opaque_data:$.opaque_data?s5($.opaque_data):null,lookup_secret_types:Z($.lookup_secret_types),lookup_scopes:Z($.lookup_scopes),lookup_names:Z($.lookup_names)}}var RB=C([K("bind_call",N(),!1),K("output_schema",N(),!1),K("bind_opaque_data",N(),!0),K("projection_ids",j(K("item",P0(),!0)),!0),K("pushdown_filters",rX(),!0),K("join_keys",j(K("item",rX(),!0)),!0),K("split_tokens",j(K("item",rX(),!0)),!0),K("row_limit",P0(),!0),K("phase",G$(F(),Y$()),!0),K("finalize_state_id",N(),!0),K("execution_id",N(),!0),K("init_opaque_data",N(),!0),K("substream_id",N(),!0),K("order_by_column_name",F(),!0),K("order_by_direction",G$(F(),Y$()),!0),K("order_by_null_order",G$(F(),Y$()),!0),K("order_by_limit",P0(),!0),K("tablesample_percentage",t5(),!0),K("tablesample_seed",P0(),!0)]),dd=C([K("execution_id",N(),!1),K("opaque_data",N(),!0),K("max_workers",P0(),!1)]);function EB($){let Z=dA($.bind_call),X={bind_call:MX(Z),output_schema:o5($.output_schema),bind_opaque_data:$.bind_opaque_data??null,projection_ids:$.projection_ids?.map((J)=>BigInt(J))??null,pushdown_filters:$.pushdown_filters?MX($.pushdown_filters):null,join_keys:($.join_keys??[]).map((J)=>MX(J)),split_tokens:$.split_tokens??null,row_limit:$.row_limit??null,phase:$.phase??null,finalize_state_id:$.finalize_state_id??null,execution_id:$.execution_id??null,init_opaque_data:$.init_opaque_data??null,substream_id:$.substream_id??null,order_by_column_name:$.order_by_column_name??null,order_by_direction:$.order_by_direction??null,order_by_null_order:$.order_by_null_order??null,order_by_limit:$.order_by_limit??null,tablesample_percentage:$.tablesample_percentage??null,tablesample_seed:$.tablesample_seed??null};return lA(RB,X)}function wB($){return{execution_id:s5($.execution_id),opaque_data:$.opaque_data?s5($.opaque_data):null,max_workers:Number($.max_workers)}}function W$($){return typeof $==="string"?[$]:Array.from($,String)}function SB($,Z){return MX(fJ([Z],$))}function HY($,Z){let Q=GY(Z);if(Q.numRows===0){let J=$.fields.map((W)=>W.name).join(",");throw Error(`decodeASD: empty batch (expected 1 row for ${J})`)}let X={};for(let J of $.fields){let W=Q.getChild(J.name),Y=W?W.get(0):null;X[J.name]=Z9(Y,J.type)}return X}function Z9($,Z){if($==null)return null;if(jw(Z)){let Q=Z.children[0].type,X=[];for(let J of $)X.push(Z9(J,Q));return X}if(Rw(Z)){let Q={};if($[Symbol.iterator]){for(let X of $)if(Array.isArray(X))Q[String(X[0])]=String(X[1]??"");else if(X&&typeof X==="object"){let J=X.key??X[0],W=X.value??X[1];Q[String(J)]=W==null?"":String(W)}}else if(typeof $==="object")for(let[X,J]of Object.entries($))Q[String(X)]=J==null?"":String(J);return Q}if(Vw(Z)){let Q=Z.children,X={};for(let J of Q)X[J.name]=Z9($[J.name],J.type);return X}if(Ew(Z))return $==null?null:String($);if(Ow(Z))return BB($);if(Q9(Z)&&Z.bitWidth===64){if(typeof $==="bigint"){if($>=BigInt(Number.MIN_SAFE_INTEGER)&&$<=BigInt(Number.MAX_SAFE_INTEGER))return Number($);return $}return $}if(Cw(Z))return Boolean($);return $}function BB($){if($ instanceof Uint8Array)return $;if($ instanceof ArrayBuffer)return new Uint8Array($);if($&&$.buffer instanceof ArrayBuffer)return new Uint8Array($.buffer,$.byteOffset,$.byteLength);return new Uint8Array(0)}var kB=C([K("name",F(),!1),K("implementation_version",F(),!0),K("data_version_spec",F(),!0),K("attach_option_specs",j(K("item",N(),!0)),!1),K("releases",j(K("item",h8([K("version",F(),!1),K("released_at",aS(rS.MICROSECOND,"UTC"),!0),K("summary",F(),!1),K("notes_url",F(),!0)]),!0)),!1),K("source_url",F(),!0)]),IB=C([K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("estimated_object_count",cZ(K("key",F(),!1),K("value",P0(),!0),!1),!0)]),_B=C([K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("name",F(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("columns",N(),!1),K("not_null_constraints",j(K("item",m6(),!0)),!1),K("unique_constraints",j(K("item",j(K("item",m6(),!0)),!0)),!1),K("check_constraints",j(K("item",F(),!0)),!1),K("primary_key_constraints",j(K("item",j(K("item",m6(),!0)),!0)),!1),K("foreign_key_constraints",j(K("item",N(),!0)),!1),K("write_result_modes",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("supports_column_statistics",Z0(),!1),K("scan_function",N(),!0),K("insert_function",N(),!0),K("update_function",N(),!0),K("delete_function",N(),!0),K("cardinality_estimate",P0(),!0),K("cardinality_max",P0(),!0),K("column_statistics",N(),!0),K("bind_result",N(),!0),K("required_filters",j(K("item",j(K("item",F(),!0)),!0)),!1)]),TB=C([K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("name",F(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("definition",F(),!1),K("column_comments",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1)]),gB=C([K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("name",F(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("function_type",G$(F(),Y$()),!1),K("arguments",N(),!1),K("output_schema",N(),!1),K("parameter_default_values",N(),!0),K("stability",G$(F(),Y$()),!0),K("null_handling",G$(F(),Y$()),!0),K("argument_monotonicity",j(K("item",F(),!0)),!0),K("description",F(),!1),K("examples",j(K("item",h8([K("sql",F(),!1),K("description",F(),!1),K("expected_output",F(),!0)]),!0)),!1),K("categories",j(K("item",F(),!0)),!1),K("projection_pushdown",Z0(),!0),K("filter_pushdown",Z0(),!0),K("sampling_pushdown",Z0(),!0),K("late_materialization",Z0(),!0),K("filter_semantic_profiles",j(K("item",F(),!0)),!1),K("additional_filter_functions",j(K("item",h8([K("namespace",F(),!1),K("name",F(),!1),K("version",PA(),!1)]),!0)),!1),K("runtime_filter_algorithms",j(K("item",h8([K("namespace",F(),!1),K("name",F(),!1),K("version",PA(),!1)]),!0)),!1),K("filter_evaluation_contexts",j(K("item",h8([K("profile",F(),!1),K("provider_fingerprint",F(),!0)]),!0)),!1),K("order_preservation",G$(F(),Y$()),!0),K("max_workers",m6(),!0),K("supports_batch_index",Z0(),!1),K("supports_splits",Z0(),!1),K("filters_exactly_applied",Z0(),!1),K("supports_positions",Z0(),!1),K("split_token_ttl_seconds",P0(),!0),K("partition_kind",G$(F(),Y$()),!1),K("order_dependent",G$(F(),Y$()),!1),K("distinct_dependent",G$(F(),Y$()),!1),K("supports_window",Z0(),!1),K("streaming_partitioned",Z0(),!1),K("has_finalize",Z0(),!1),K("source_order_dependent",Z0(),!1),K("sink_order_dependent",Z0(),!1),K("requires_input_batch_index",Z0(),!1),K("input_from_args",Z0(),!1),K("required_settings",j(K("item",F(),!0)),!1),K("required_secrets",j(K("item",h8([K("secret_type",F(),!1),K("scope",F(),!0),K("secret_name",F(),!0)]),!0)),!1)]),xB=C([K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("name",F(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("macro_type",G$(F(),Y$()),!1),K("parameters",j(K("item",F(),!0)),!1),K("parameter_default_values",N(),!0),K("definition",F(),!1),K("arguments_schema",N(),!0)]),nd=C([K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("name",F(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("table_name",F(),!1),K("index_type",F(),!1),K("constraint_type",G$(F(),Y$()),!1),K("expressions",j(K("item",F(),!0)),!1),K("options",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1)]),od=C([K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("format_name",F(),!1),K("handler",F(),!1),K("options",N(),!1),K("direction",F(),!1),K("description",F(),!1),K("ordered",Z0(),!1)]),sd=C([K("function_name",F(),!1),K("arguments",N(),!1),K("required_extensions",j(K("item",F(),!0)),!1),K("schema_path",j(K("item",F(),!0)),!0)]),id=C([K("branches",j(K("item",N(),!0)),!1),K("required_extensions",j(K("item",F(),!0)),!1)]),rd=C([K("function_name",F(),!1),K("arguments",N(),!1),K("branch_filter",F(),!0),K("writable",Z0(),!1),K("source_catalog",F(),!0),K("source_schema_path",j(K("item",F(),!0)),!0),K("source_table",F(),!0),K("format_name",F(),!0),K("format_locations",j(K("item",F(),!0)),!0),K("format_options",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),ad=C([K("payload",N(),!1),K("token",N(),!1),K("estimated_rows",P0(),!0),K("rows_exact",Z0(),!1),K("estimated_bytes",P0(),!0),K("partition_bounds",N(),!0),K("column_statistics",N(),!0),K("location_ids",j(K("item",P0(),!0)),!0),K("start_position",N(),!0),K("end_position",N(),!0)]),td=C([K("alias",F(),!1),K("target",F(),!1),K("db_type",F(),!1),K("options",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("hidden",Z0(),!1),K("required",Z0(),!1),K("secret_ref",F(),!1)]),ed=C([K("fk_columns",j(K("item",F(),!0)),!1),K("pk_columns",j(K("item",F(),!0)),!1),K("referenced_table",F(),!1),K("referenced_schema_path",j(K("item",F(),!0)),!1)]),$n=C([K("function_name",F(),!1),K("arguments",N(),!1),K("input_schema",N(),!0),K("settings",N(),!0),K("secrets",N(),!0),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0),K("argument_names",j(K("item",F(),!0)),!0)]),Zn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("merge_batch",N(),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Qn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("group_ids_batch",N(),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Xn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("group_ids_batch",N(),!1),K("output_schema",N(),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Jn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("input_batch",N(),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Wn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Yn=C([K("function_name",F(),!1),K("arguments",N(),!1),K("input_schema",N(),!1),K("partition_key_count",P0(),!1),K("order_key_count",P0(),!1),K("output_schema",N(),!1),K("settings",N(),!0),K("secrets",N(),!0),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Gn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("input_batch",N(),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Kn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("partition_id",P0(),!1),K("row_idx",P0(),!1),K("count",P0(),!1),K("frames_per_row",j(K("item",P0(),!0)),!1),K("frame_starts",j(K("item",P0(),!0)),!1),K("frame_ends",j(K("item",P0(),!0)),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Hn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("partition_id",P0(),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),zn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("partition_id",P0(),!1),K("row_count",P0(),!1),K("partition_batch",N(),!1),K("output_schema",N(),!1),K("filter_mask",N(),!1),K("frame_stats",N(),!1),K("all_valid",N(),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),qn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("partition_id",P0(),!1),K("rid",P0(),!1),K("frame_starts",j(K("item",P0(),!0)),!1),K("frame_ends",j(K("item",P0(),!0)),!1),K("attach_opaque_data",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Pn=C([K("function_name",F(),!1),K("arguments",N(),!1),K("function_type",G$(F(),Y$()),!1),K("input_schema",N(),!0),K("settings",N(),!0),K("secrets",N(),!0),K("attach_opaque_data",N(),!0),K("transaction_opaque_data",N(),!0),K("resolved_secrets_provided",Z0(),!1),K("at_unit",F(),!0),K("at_value",F(),!0),K("copy_from",h8([K("format",F(),!1),K("file_path",F(),!1),K("expected_schema",N(),!1)]),!0),K("copy_to",h8([K("format",F(),!1),K("file_path",F(),!1)]),!0),K("schema_path",j(K("item",F(),!0)),!0),K("argument_names",j(K("item",F(),!0)),!0)]),Fn=C([K("name",F(),!1),K("options",N(),!0),K("data_version_spec",F(),!0),K("implementation_version",F(),!0),K("client_capabilities",N(),!0)]),Nn=C([K("name",F(),!1),K("on_conflict",G$(F(),Y$()),!1),K("options",N(),!0)]),vB=C([K("engine",F(),!1),K("native_formats",j(K("item",F(),!0)),!1),K("catalogs",j(K("item",F(),!0)),!1),K("can_stream",Z0(),!1),K("filter_encodings",j(K("item",F(),!0)),!1)]),Un=C([K("format",F(),!1),K("file_path",F(),!1),K("expected_schema",N(),!1)]),Mn=C([K("format",F(),!1),K("file_path",F(),!1)]),Ln=C([K("execution_id",N(),!1),K("opaque_data",N(),!0),K("max_workers",P0(),!1)]),An=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("table_name",F(),!1),K("index_type",F(),!1),K("constraint_type",G$(F(),Y$()),!1),K("expressions",j(K("item",F(),!0)),!1),K("on_conflict",G$(F(),Y$()),!1),K("options",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("transaction_opaque_data",N(),!0)]),Dn=C([K("bind_call",N(),!1),K("output_schema",N(),!1),K("bind_opaque_data",N(),!0),K("projection_ids",j(K("item",P0(),!0)),!0),K("pushdown_filters",rX(),!0),K("join_keys",j(K("item",rX(),!0)),!0),K("split_tokens",j(K("item",rX(),!0)),!0),K("row_limit",P0(),!0),K("phase",G$(F(),Y$()),!0),K("finalize_state_id",N(),!0),K("execution_id",N(),!0),K("init_opaque_data",N(),!0),K("substream_id",N(),!0),K("order_by_column_name",F(),!0),K("order_by_direction",G$(F(),Y$()),!0),K("order_by_null_order",G$(F(),Y$()),!0),K("order_by_limit",P0(),!0),K("tablesample_percentage",t5(),!0),K("tablesample_seed",P0(),!0)]),yB=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("macro_type",G$(F(),Y$()),!1),K("parameters",j(K("item",F(),!0)),!1),K("definition",F(),!1),K("on_conflict",G$(F(),Y$()),!1),K("parameter_default_values",N(),!0),K("arguments_schema",N(),!0),K("transaction_opaque_data",N(),!0)]),On=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("columns",N(),!1),K("on_conflict",G$(F(),Y$()),!1),K("not_null_constraints",j(K("item",m6(),!0)),!1),K("unique_constraints",j(K("item",j(K("item",m6(),!0)),!0)),!1),K("check_constraints",j(K("item",F(),!0)),!1),K("primary_key_constraints",j(K("item",j(K("item",m6(),!0)),!0)),!1),K("foreign_key_constraints",j(K("item",N(),!0)),!1),K("transaction_opaque_data",N(),!0)]),Cn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("state_ids",j(K("item",N(),!0)),!1),K("attach_opaque_data",N(),!0),K("transaction_id",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),jn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("attach_opaque_data",N(),!0),K("transaction_id",N(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Vn=C([K("function_name",F(),!1),K("execution_id",N(),!1),K("input_batch",N(),!1),K("attach_opaque_data",N(),!0),K("transaction_id",N(),!0),K("batch_index",P0(),!0),K("schema_path",j(K("item",F(),!0)),!0)]),Rn=C([K("bind_call",N(),!1),K("bind_opaque_data",N(),!0)]),En=C([K("bind_call",N(),!1),K("bind_opaque_data",N(),!0),K("global_execution_id",N(),!1)]),wn=C([K("bind_call",N(),!1),K("bind_opaque_data",N(),!0),K("projection_ids",j(K("item",P0(),!0)),!0),K("pushdown_filters",rX(),!0),K("join_keys",j(K("item",rX(),!0)),!0),K("row_limit",P0(),!0),K("target_split_bytes",P0(),!0),K("min_splits",P0(),!0),K("max_splits_per_response",P0(),!0),K("cursor",N(),!0),K("refined_filters",rX(),!0),K("filters_complete",Z0(),!1),K("start_position",N(),!0),K("end_position",N(),!0),K("order_by_column_name",F(),!0),K("order_by_direction",G$(F(),Y$()),!0),K("order_by_null_order",G$(F(),Y$()),!0),K("order_by_limit",P0(),!0),K("tablesample_percentage",t5(),!0),K("tablesample_seed",P0(),!0)]),Sn=C([K("bind_call",N(),!1),K("bind_opaque_data",N(),!0)]),Bn=C([K("output_schema",N(),!1),K("execution_id",N(),!1)]),kn=C([]),In=C([]),_n=C([K("result_batch",N(),!1)]),Tn=C([K("result_batch",N(),!1)]),gn=C([]),xn=C([K("execution_id",N(),!1)]),vn=C([]),yn=C([K("result_batch",N(),!1)]),hn=C([K("result_batch",N(),!1)]),bn=C([]),mn=C([]),fn=C([K("output_schema",N(),!1),K("opaque_data",N(),!0),K("lookup_secret_types",j(K("item",F(),!0)),!1),K("lookup_scopes",j(K("item",F(),!0)),!1),K("lookup_names",j(K("item",F(),!0)),!1)]),un=C([K("attach_opaque_data",N(),!1),K("supports_transactions",Z0(),!1),K("supports_time_travel",Z0(),!1),K("catalog_version_frozen",Z0(),!1),K("catalog_version",P0(),!1),K("attach_opaque_data_required",Z0(),!1),K("default_schema",F(),!1),K("settings",j(K("item",N(),!0)),!1),K("secret_types",j(K("item",N(),!0)),!1),K("attach_catalogs",j(K("item",N(),!0)),!1),K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!1),K("supports_column_statistics",Z0(),!1),K("global_functions",j(K("item",N(),!0)),!1),K("global_function_prefix",F(),!1),K("resolved_data_version",F(),!0),K("resolved_implementation_version",F(),!0)]),pn=C([K("items",j(K("item",N(),!0)),!1)]),cn=C([K("items",j(K("item",N(),!0)),!1)]),ln=C([K("items",j(K("item",N(),!0)),!1)]),dn=C([K("items",j(K("item",N(),!0)),!1)]),nn=C([K("items",j(K("item",N(),!0)),!1)]),on=C([K("items",j(K("item",N(),!0)),!1)]),sn=C([K("items",j(K("item",N(),!0)),!1)]),rn=C([K("items",j(K("item",N(),!0)),!1)]),an=C([K("items",j(K("item",N(),!0)),!1)]),tn=C([K("items",j(K("item",N(),!0)),!1)]),en=C([K("items",j(K("item",N(),!0)),!1)]),$o=C([K("items",j(K("item",N(),!0)),!1)]),Zo=C([K("transaction_opaque_data",N(),!0)]),Qo=C([K("version",P0(),!1)]),Xo=C([K("items",j(K("item",N(),!0)),!1)]),Jo=C([K("finalize_state_ids",j(K("item",N(),!0)),!1)]),Wo=C([]),Yo=C([K("state_id",N(),!1)]),Go=C([K("estimate",P0(),!0),K("max",P0(),!0)]),Ko=C([K("keys",j(K("item",F(),!0)),!1),K("values",j(K("item",F(),!0)),!1)]),Ho=C([K("splits",j(K("item",N(),!0)),!1),K("next_cursors",j(K("item",N(),!0)),!0),K("execution_id",N(),!0),K("init_opaque_data",N(),!0),K("max_workers",P0(),!0),K("estimated_total_splits",P0(),!0),K("estimated_total_rows",P0(),!0),K("estimated_total_bytes",P0(),!0),K("catalog_version",P0(),!0),K("scope",F(),!1),K("locations",j(K("item",F(),!0)),!0),K("partitioning",j(K("item",N(),!0)),!0),K("sort_order",j(K("item",N(),!0)),!0),K("cache_max_age_seconds",P0(),!0),K("start_position",N(),!0),K("end_position",N(),!0)]),zo=C([K("request",N(),!1)]),qo=C([K("request",N(),!1)]),Po=C([K("request",N(),!1)]),Fo=C([K("request",N(),!1)]),No=C([K("request",N(),!1)]),Uo=C([K("request",N(),!1)]),Mo=C([K("request",N(),!1)]),Lo=C([K("request",N(),!1)]),Ao=C([K("request",N(),!1)]),Do=C([K("request",N(),!1)]),Oo=C([K("request",N(),!1)]),Co=C([K("request",N(),!1)]),jo=C([K("request",N(),!1)]),Vo=C([K("request",N(),!1)]),Ro=C([]),Eo=C([K("attach_opaque_data",N(),!1),K("transaction_opaque_data",N(),!0)]),wo=C([K("request",N(),!1)]),So=C([K("attach_opaque_data",N(),!1)]),Bo=C([K("name",F(),!1)]),ko=C([K("request",N(),!1)]),Io=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("ignore_not_found",Z0(),!1),K("cascade",Z0(),!1),K("transaction_opaque_data",N(),!0)]),_o=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("transaction_opaque_data",N(),!0)]),To=C([K("request",N(),!1)]),go=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),xo=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("transaction_opaque_data",N(),!0)]),vo=C([K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("type",G$(F(),Y$()),!1),K("transaction_opaque_data",N(),!0)]),yo=C([K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("transaction_opaque_data",N(),!0)]),ho=C([K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("type",G$(F(),Y$()),!1),K("transaction_opaque_data",N(),!0)]),bo=C([K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("transaction_opaque_data",N(),!0)]),mo=C([K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("transaction_opaque_data",N(),!0)]),fo=C([K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("on_conflict",G$(F(),Y$()),!1),K("comment",F(),!0),K("tags",cZ(K("key",F(),!1),K("value",F(),!0),!1),!0),K("transaction_opaque_data",N(),!0)]),uo=C([K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("ignore_not_found",Z0(),!1),K("cascade",Z0(),!1),K("transaction_opaque_data",N(),!0)]),po=C([K("attach_opaque_data",N(),!1),K("path",j(K("item",F(),!0)),!1),K("transaction_opaque_data",N(),!0)]),co=C([K("attach_opaque_data",N(),!1),K("transaction_opaque_data",N(),!0)]),lo=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_definition",N(),!1),K("ignore_not_found",Z0(),!1),K("if_column_not_exists",Z0(),!1),K("transaction_opaque_data",N(),!0)]),no=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_name",F(),!1),K("comment",F(),!0),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),oo=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_name",F(),!1),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),so=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_name",F(),!1),K("expression",F(),!1),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),io=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_name",F(),!1),K("ignore_not_found",Z0(),!1),K("if_column_exists",Z0(),!1),K("cascade",Z0(),!1),K("transaction_opaque_data",N(),!0)]),ro=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_name",F(),!1),K("new_column_name",F(),!1),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),ao=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("transaction_opaque_data",N(),!0)]),to=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_definition",N(),!1),K("expression",F(),!0),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),eo=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("comment",F(),!0),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),$s=C([K("request",N(),!1)]),Zs=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("transaction_opaque_data",N(),!0)]),Qs=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("ignore_not_found",Z0(),!1),K("cascade",Z0(),!1),K("transaction_opaque_data",N(),!0)]),Xs=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("at_unit",F(),!0),K("at_value",F(),!0),K("transaction_opaque_data",N(),!0)]),Js=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("transaction_opaque_data",N(),!0),K("writable_branch_function_name",F(),!0)]),Ws=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_name",F(),!1),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),Ys=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("column_name",F(),!1),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),Gs=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("new_name",F(),!1),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),Ks=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("at_unit",F(),!0),K("at_value",F(),!0),K("transaction_opaque_data",N(),!0)]),Hs=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("at_unit",F(),!0),K("at_value",F(),!0),K("transaction_opaque_data",N(),!0)]),zs=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("transaction_opaque_data",N(),!0)]),qs=C([K("attach_opaque_data",N(),!1)]),Ps=C([K("attach_opaque_data",N(),!1),K("transaction_opaque_data",N(),!1)]),Fs=C([K("attach_opaque_data",N(),!1),K("transaction_opaque_data",N(),!1)]),Ns=C([K("attach_opaque_data",N(),!1),K("transaction_opaque_data",N(),!0)]),Us=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("comment",F(),!0),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),Ms=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("definition",F(),!1),K("on_conflict",G$(F(),Y$()),!1),K("transaction_opaque_data",N(),!0)]),Ls=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("ignore_not_found",Z0(),!1),K("cascade",Z0(),!1),K("transaction_opaque_data",N(),!0)]),As=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("transaction_opaque_data",N(),!0)]),Ds=C([K("attach_opaque_data",N(),!1),K("schema_path",j(K("item",F(),!0)),!1),K("name",F(),!1),K("new_name",F(),!1),K("ignore_not_found",Z0(),!1),K("transaction_opaque_data",N(),!0)]),Os=C([K("request",N(),!1)]),Cs=C([K("request",N(),!1)]),js=C([K("request",N(),!1)]),Vs=C([K("request",N(),!1)]),Rs=C([K("request",N(),!1)]),Es=C([K("request",N(),!1)]),ws=C([K("request",N(),!1)]),Ss=C([K("request",N(),!1)]),hB=($)=>HY(kB,$);var bB=($)=>HY(gB,$);var UA=($)=>HY(xB,$);var MA=($)=>HY(IB,$);var LA=($)=>HY(_B,$);var AA=($)=>HY(TB,$);function mB($){let Z=$.arguments,Q=[],X={};if(Z!=null){let Y=GY(K8(Z));for(let G of Y.schema.fields){let H=Y.getChild(G.name),z=H?H.get(0):null;if(G.name.startsWith("arg_")){let q=parseInt(G.name.slice(4),10);while(Q.length<=q)Q.push(null);Q[q]=z}else X[G.name]=z}}let J=$.required_extensions,W=J==null?[]:(Array.isArray(J)?J:[...J]).filter((Y)=>Y!=null).map(String);return{functionName:String($.function_name??""),positionalArguments:Q,namedArguments:X,requiredExtensions:W,schemaPath:$.schema_path==null?null:(Array.isArray($.schema_path)?$.schema_path:[...$.schema_path]).filter((Y)=>Y!=null).map(String)}}function l5($){return{request:MX($)}}function pZ($){let Z=K8($.result),Q=GY(Z);return bA(Q)}function fB($){if($===null)return pA();if(typeof $==="string")return F();if(typeof $==="bigint")return P0();if(typeof $==="number"){if(!Number.isFinite($))throw TypeError(`Attach option number values must be finite (got ${$}). Use a bigint for integers or a finite number for floats.`);return t5()}if(typeof $==="boolean")return Z0();if($ instanceof Uint8Array)return N();throw TypeError(`Unsupported attach option value type: ${typeof $}. Supported: string, number, bigint, boolean, null, Uint8Array.`)}function DA($){if(!$)return null;let Z=Object.entries($);if(Z.length===0)return null;let Q=[],X={};for(let[Y,G]of Z){let H=fB(G);Q.push(K(Y,H,!0)),X[Y]=[G]}let J=C(Q),W=lJ(X,J);return MX(W)}class d$ extends Error{remoteTraceback;errorType;constructor($,Z){super($,Z?.cause!=null?{cause:Z.cause}:void 0);if(this.name="VgiClientError",Z?.remoteTraceback)this.remoteTraceback=Z.remoteTraceback;if(Z?.errorType)this.errorType=Z.errorType}static fromRpcError($){let Z=`${$.errorType}: ${$.errorMessage}`,Q=$.remoteTraceback?`${Z} +]`}concat(...t){let e=this.schema,n=this.data.concat(t.flatMap(({data:r})=>r));return new Xt(e,n.map((r)=>new rt(e,r)))}slice(t,e){let n=this.schema;[t,e]=Fo({length:this.numRows},t,e);let r=Ec(this.data,this._offsets,t,e);return new Xt(n,r.map((a)=>new rt(n,a)))}getChild(t){return this.getChildAt(this.schema.fields.findIndex((e)=>e.name===t))}getChildAt(t){if(t>-1&&tn.children[t]);if(e.length===0){let{type:n}=this.schema.fields[t],r=A({type:n,length:0,nullCount:0});e.push(r._changeLengthAndBackfillNullBitmap(this.numRows))}return new O(e)}return null}setChild(t,e){var n;return this.setChildAt((n=this.schema.fields)===null||n===void 0?void 0:n.findIndex((r)=>r.name===t),e)}setChildAt(t,e){let n=this.schema,r=[...this.batches];if(t>-1&&tthis.getChildAt(l));[a[t],s[t]]=[o,e],[n,r]=pl(n,s)}return new Xt(n,r)}select(t){let e=this.schema.fields.reduce((n,r,a)=>n.set(r.name,a),new Map);return this.selectAt(t.map((n)=>e.get(n)).filter((n)=>n>-1))}selectAt(t){let e=this.schema.selectAt(t),n=this.batches.map((r)=>r.selectAt(t));return new Xt(e,n)}assign(t){let e=this.schema.fields,[n,r]=t.schema.fields.reduce((s,c,l)=>{let[f,p]=s,_=e.findIndex((g)=>g.name===c.name);return~_?p[_]=l:f.push(l),s},[[],[]]),a=this.schema.assign(t.schema),o=[...e.map((s,c)=>[c,r[c]]).map(([s,c])=>c===void 0?this.getChildAt(s):t.getChildAt(c)),...n.map((s)=>t.getChildAt(s))].filter(Boolean);return new Xt(...pl(a,o))}};jy=Symbol.toStringTag;Xt[jy]=((t)=>(t.schema=null,t.batches=[],t._offsets=new Uint32Array([0]),t._nullCount=-1,t[Symbol.isConcatSpreadable]=!0,t[$y]=!0,t.isValid=Fa(Lo),t.get=Fa(re.getVisitFn(y.Struct)),t.set=Dc(Be.getVisitFn(y.Struct)),t.indexOf=Bc(Qi.getVisitFn(y.Struct)),"Table"))(Xt.prototype);Object.defineProperty(Xt,Symbol.hasInstance,{value:function(e){return Function.prototype[Symbol.hasInstance].call(this,e)||this===Xt&&Xt.isTable(e)}})});function Hy(t,e,n=e.reduce((r,a)=>Math.max(r,a.length),0)){var r;let a=[...t.fields],o=[...e],s=(n+63&-64)>>3;for(let[c,l]of t.fields.entries()){let f=e[c];if(!f||f.length!==n)a[c]=l.clone({nullable:!0}),o[c]=(r=f===null||f===void 0?void 0:f._changeLengthAndBackfillNullBitmap(n))!==null&&r!==void 0?r:A({type:l.type,length:n,nullCount:n,nullBitmap:new Uint8Array(s)})}return[t.assign(a),A({type:new it(a),length:n,children:o})]}function Gy(t,e,n=new Map){var r,a;if(((r=t===null||t===void 0?void 0:t.length)!==null&&r!==void 0?r:0)>0&&(t===null||t===void 0?void 0:t.length)===(e===null||e===void 0?void 0:e.length))for(let o=-1,s=t.length;++o{zn();hl();je();$e();Ft();Ra();Yi();bn();Rc();Oc();Ky=Symbol.for("apache-arrow/RecordBatch");rt=class rt{static isRecordBatch(t){return(t===null||t===void 0?void 0:t[Ky])===!0}constructor(...t){switch(t.length){case 3:case 2:{if([this.schema]=t,!(this.schema instanceof P))throw TypeError("RecordBatch constructor expects a [Schema, Data] pair.");if([,this.data=A({nullCount:0,type:new it(this.schema.fields),children:this.schema.fields.map((e)=>A({type:e.type,nullCount:0}))}),this._metadata=new Map]=t,!(this.data instanceof tt))throw TypeError("RecordBatch constructor expects a [Schema, Data] pair.");[this.schema,this.data]=Hy(this.schema,this.data.children,this.data.length);break}case 1:{let[e]=t,{fields:n,children:r,length:a}=Object.keys(e).reduce((c,l,f)=>(c.children[f]=e[l],c.length=Math.max(c.length,e[l].length),c.fields[f]=N.new({name:l,type:e[l].type,nullable:!0}),c),{length:0,fields:[],children:[]}),o=new P(n),s=A({type:new it(n),length:a,children:r,nullCount:0});[this.schema,this.data]=Hy(o,s.children,a),this._metadata=new Map;break}default:throw TypeError("RecordBatch constructor expects an Object mapping names to child Data, or a [Schema, Data] pair.")}}get metadata(){return this._metadata}get dictionaries(){return this._dictionaries||(this._dictionaries=Gy(this.schema.fields,this.data.children))}get numCols(){return this.schema.fields.length}get numRows(){return this.data.length}get nullCount(){return this.data.nullCount}isValid(t){return this.data.getValid(t)}get(t){return re.visit(this.data,t)}at(t){return this.get(Ki(t,this.numRows))}set(t,e){return Be.visit(this.data,t,e)}indexOf(t,e){return Qi.visit(this.data,t,e)}[Symbol.iterator](){return Ua.visit(new O([this.data]))}toArray(){return[...this]}concat(...t){return new Xt(this.schema,[this,...t])}slice(t,e){let[n]=new O([this.data]).slice(t,e).data;return new rt(this.schema,n,this._metadata)}getChild(t){var e;return this.getChildAt((e=this.schema.fields)===null||e===void 0?void 0:e.findIndex((n)=>n.name===t))}getChildAt(t){if(t>-1&&tr.name===t),e)}setChildAt(t,e){let n=this.schema,r=this.data;if(t>-1&&ts.name===a);if(~o)r[o]=this.data.children[o]}return new rt(e,A({type:n,length:this.numRows,children:r}),this._metadata)}selectAt(t){let e=this.schema.selectAt(t),n=t.map((a)=>this.data.children[a]).filter(Boolean),r=A({type:new it(e.fields),length:this.numRows,children:n});return new rt(e,r,this._metadata)}};Yy=Symbol.toStringTag;rt[Yy]=((t)=>(t._nullCount=-1,t[Symbol.isConcatSpreadable]=!0,t[Ky]=!0,"RecordBatch"))(rt.prototype);Object.defineProperty(rt,Symbol.hasInstance,{value:function(e){return Function.prototype[Symbol.hasInstance].call(this,e)||this===rt&&rt.isRecordBatch(e)}});ja=class ja extends rt{constructor(t,e){let n=t.fields.map((a)=>A({type:a.type})),r=A({type:new it(t.fields),length:0,nullCount:0,children:n});super(t,r,e||new Map)}}});function gl(t,e=0){for(let n=-1,r=$a.length;++n`Expected ${pt[t]} Message in stream, but was null or length 0.`,Id=(t)=>`Header pointer of flatbuffer-encoded ${pt[t]} Message is null or length 0.`,Wy=(t,e)=>`Expected to read ${t} metadata bytes, but only read ${e}.`,Qy=(t,e)=>`Expected to read ${t} bytes for message body, but only read ${e}.`,Qo,ml,yl,_l=4,Sd="ARROW1",$a,Ha,Td,Xy;var vd=b(()=>{un();ne();ht();Xi();Vf();oe();Ji();Vo();Qo=class Qo{constructor(t){this.source=t instanceof Wr?t:new Wr(t)}[Symbol.iterator](){return this}next(){let t;if((t=this.readMetadataLength()).done)return Pt;if(t.value===-1&&(t=this.readMetadataLength()).done)return Pt;if((t=this.readMetadata(t.value)).done)return Pt;return t}throw(t){return this.source.throw(t)}return(t){return this.source.return(t)}readMessage(t){let e;if((e=this.next()).done)return null;if(t!=null&&e.value.headerType!==t)throw Error(Ad(t));return e.value}readMessageBody(t){if(t<=0)return new Uint8Array(0);let e=L(this.source.read(t));if(e.byteLength[...r,...a.VALIDITY&&[a.VALIDITY]||[],...a.TYPE_ID&&[a.TYPE_ID]||[],...a.OFFSET&&[a.OFFSET]||[],...a.DATA&&[a.DATA]||[],...a.VIEWS&&[a.VIEWS]||[],...a.VARIADIC_DATA_BUFFERS||[],...e(a.children)],[])}}readMessage(t){let e;if((e=this.next()).done)return null;if(t!=null&&e.value.headerType!==t)throw Error(Ad(t));return e.value}readSchema(){let t=pt.Schema,e=this.readMessage(t),n=e===null||e===void 0?void 0:e.header();if(!e||!n)throw Error(Id(t));return n}};$a=new Uint8Array(Sd.length);for(let t=0;t=this.MIN_HEADER_LENGTH}_hasValidMagicNumber(t){return this.LZ4_FRAME_MAGIC.every((e,n)=>t[n]===e)}_hasValidVersion(t){return(t[4]&192)>>6===1}}class Zy{constructor(){this.ZSTD_MAGIC=new Uint8Array([40,181,47,253]),this.MIN_HEADER_LENGTH=6}isValidCodecEncode(t){let e=new Uint8Array([1,2,3,4,5,6,7,8]),n=t.encode(e);return this._isValidCompressed(n)}_isValidCompressed(t){return this._hasMinimumLength(t)&&this._hasValidMagicNumber(t)}_hasMinimumLength(t){return t.length>=this.MIN_HEADER_LENGTH}_hasValidMagicNumber(t){return this.ZSTD_MAGIC.every((e,n)=>t[n]===e)}}var t_;var e_=b(()=>{Io();t_={[qe.LZ4_FRAME]:new Jy,[qe.ZSTD]:new Zy}});class n_{constructor(){this.registry={}}set(t,e){if((e===null||e===void 0?void 0:e.encode)&&typeof e.encode==="function"&&!t_[t].isValidCodecEncode(e))throw Error(`Encoder for ${qe[t]} is not valid.`);this.registry[t]=e}get(t){var e;return((e=this.registry)===null||e===void 0?void 0:e[t])||null}}var Ya;var Ed=b(()=>{Io();e_();Ya=new n_});var bl=-1,Xo=8;class Dd{get numDictionaries(){return this._dictionaryIndex}get numRecordBatches(){return this._recordBatchIndex}constructor(t=new Map){this.closed=!1,this.autoDestroy=!0,this._dictionaryIndex=0,this._recordBatchIndex=0,this.dictionaries=t}isSync(){return!1}isAsync(){return!1}isFile(){return!1}isStream(){return!1}reset(t){return this._dictionaryIndex=0,this._recordBatchIndex=0,this.schema=t,this.dictionaries=new Map,this}_loadRecordBatch(t,e,n){let r;if(t.compression!=null){let o=Ya.get(t.compression.type);if((o===null||o===void 0?void 0:o.decode)&&typeof o.decode==="function"){let{decommpressedBody:s,buffers:c}=this._decompressBuffers(t,e,o);r=this._loadCompressedVectors(t,s,this.schema.fields),t=new le(t.length,t.nodes,c,null)}else throw Error("Record batch is compressed but codec not found")}else r=this._loadVectors(t,e,this.schema.fields);let a=A({type:new it(this.schema.fields),length:t.length,children:r});return new rt(this.schema,a,n)}_loadDictionaryBatch(t,e){let{id:n,isDelta:r}=t,{dictionaries:a,schema:o}=this,s=a.get(n),c=o.dictionaries.get(n),l;if(t.data.compression!=null){let f=Ya.get(t.data.compression.type);if((f===null||f===void 0?void 0:f.decode)&&typeof f.decode==="function"){let{decommpressedBody:p,buffers:_}=this._decompressBuffers(t.data,e,f);l=this._loadCompressedVectors(t.data,p,[c]),t=new He(new le(t.data.length,t.data.nodes,_,null,t.data.variadicBufferCounts),n,r)}else throw Error("Dictionary batch is compressed but codec not found")}else l=this._loadVectors(t.data,e,[c]);return(s&&r?s.concat(new O(l)):new O(l)).memoize()}_loadVectors(t,e,n){return new qo(e,t.nodes,t.buffers,this.dictionaries,this.schema.metadataVersion,t.variadicBufferCounts).visitMany(n)}_loadCompressedVectors(t,e,n){return new Gf(e,t.nodes,t.buffers,this.dictionaries,this.schema.metadataVersion,t.variadicBufferCounts).visitMany(n)}_decompressBuffers(t,e,n){let r=[],a=[],o=0;for(let{offset:s,length:c}of t.buffers){if(c===0){r.push(new Uint8Array(0)),a.push(new Qt(o,0));continue}let l=new Ie(e.subarray(s,s+c)),f=gt(l.readInt64(0)),p=l.bytes().subarray(Xo),_=f===bl?p:n.decode(p);r.push(_);let g=(o+7&-8)-o;o+=g,a.push(new Qt(o,_.length)),o+=_.length}return{decommpressedBody:r,buffers:a}}}function s_(t,e){return e&&typeof e.autoDestroy==="boolean"?e.autoDestroy:t.autoDestroy}function*r_(t){let e=Ot.from(t);try{if(!e.open({autoDestroy:!1}).closed)do yield e;while(!e.reset().open().closed)}finally{e.cancel()}}function i_(t){return ln(this,arguments,function*(){let n=yield st(Ot.from(t));try{if(!(yield st(n.open({autoDestroy:!1}))).closed)do yield yield st(n);while(!(yield st(n.reset().open())).closed)}finally{yield st(n.cancel())}})}function pS(t){return new Zr(new o_(t))}function hS(t){let e=t.peek(Ha+7&-8);return e&&e.byteLength>=4?!gl(e)?new Zr(new Jo(t)):new ra(new Bd(t.read())):new Zr(new Jo(function*(){}()))}function mS(t){return R(this,void 0,void 0,function*(){let e=yield t.peek(Ha+7&-8);return e&&e.byteLength>=4?!gl(e)?new na(new Zo(t)):new ra(new Bd(yield t.read())):new na(new Zo(function(){return ln(this,arguments,function*(){})}()))})}function yS(t){return R(this,void 0,void 0,function*(){let{size:e}=yield t.stat(),n=new Zi(t,e);if(e>=Xy&&gl(yield n.readAt(0,Ha+7&-8)))return new wl(new a_(n));return new na(new Zo(n))})}var Ot,Zr,na,ra,wl,Jo,Zo,Bd,a_,o_;var xl=b(()=>{un();zn();je();Ft();ne();Lf();wo();Xi();Ji();Vf();_y();Bi();Vo();vd();Ed();ht();Ot=class Ot extends Pa{constructor(t){super();this._impl=t}get closed(){return this._impl.closed}get schema(){return this._impl.schema}get autoDestroy(){return this._impl.autoDestroy}get dictionaries(){return this._impl.dictionaries}get numDictionaries(){return this._impl.numDictionaries}get numRecordBatches(){return this._impl.numRecordBatches}get footer(){return this._impl.isFile()?this._impl.footer:null}isSync(){return this._impl.isSync()}isAsync(){return this._impl.isAsync()}isFile(){return this._impl.isFile()}isStream(){return this._impl.isStream()}next(){return this._impl.next()}throw(t){return this._impl.throw(t)}return(t){return this._impl.return(t)}cancel(){return this._impl.cancel()}reset(t){return this._impl.reset(t),this._DOMStream=void 0,this._nodeStream=void 0,this}open(t){let e=this._impl.open(t);return Gn(e)?e.then(()=>this):this}readRecordBatch(t){return this._impl.isFile()?this._impl.readRecordBatch(t):null}[Symbol.iterator](){return this._impl[Symbol.iterator]()}[Symbol.asyncIterator](){return this._impl[Symbol.asyncIterator]()}toDOMStream(){return Ae.toDOMStream(this.isSync()?{[Symbol.iterator]:()=>this}:{[Symbol.asyncIterator]:()=>this})}toNodeStream(){return Ae.toNodeStream(this.isSync()?{[Symbol.iterator]:()=>this}:{[Symbol.asyncIterator]:()=>this},{objectMode:!0})}static throughNode(t){throw Error('"throughNode" not available in this environment')}static throughDOM(t,e){throw Error('"throughDOM" not available in this environment')}static from(t){if(t instanceof Ot)return t;else if(js(t))return pS(t);else if(Hs(t))return yS(t);else if(Gn(t))return(()=>R(this,void 0,void 0,function*(){return yield Ot.from(yield t)}))();else if(Ys(t)||go(t)||Gs(t)||En(t))return mS(new jn(t));return hS(new Wr(t))}static readAll(t){if(t instanceof Ot)return t.isSync()?r_(t):i_(t);else if(js(t)||ArrayBuffer.isView(t)||Wn(t)||$s(t))return r_(t);return i_(t)}};Zr=class Zr extends Ot{constructor(t){super(t);this._impl=t}readAll(){return[...this]}[Symbol.iterator](){return this._impl[Symbol.iterator]()}[Symbol.asyncIterator](){return ln(this,arguments,function*(){yield st(yield*_a(Kn(this[Symbol.iterator]())))})}};na=class na extends Ot{constructor(t){super(t);this._impl=t}readAll(){return R(this,void 0,void 0,function*(){var t,e,n,r;let a=[];try{for(var o=!0,s=Kn(this),c;c=yield s.next(),t=c.done,!t;o=!0){r=c.value,o=!1;let l=r;a.push(l)}}catch(l){e={error:l}}finally{try{if(!o&&!t&&(n=s.return))yield n.call(s)}finally{if(e)throw e.error}}return a})}[Symbol.iterator](){throw Error("AsyncRecordBatchStreamReader is not Iterable")}[Symbol.asyncIterator](){return this._impl[Symbol.asyncIterator]()}};ra=class ra extends Zr{constructor(t){super(t);this._impl=t}};wl=class wl extends na{constructor(t){super(t);this._impl=t}};Jo=class Jo extends Dd{constructor(t,e){super(e);this._reader=!js(t)?new Qo(this._handle=t):new yl(this._handle=t)}isSync(){return!0}isStream(){return!0}[Symbol.iterator](){return this}cancel(){if(!this.closed&&(this.closed=!0))this.reset()._reader.return(),this._reader=null,this.dictionaries=null}open(t){if(!this.closed){if(this.autoDestroy=s_(this,t),!(this.schema||(this.schema=this._reader.readSchema())))this.cancel()}return this}throw(t){if(!this.closed&&this.autoDestroy&&(this.closed=!0))return this.reset()._reader.throw(t);return Pt}return(t){if(!this.closed&&this.autoDestroy&&(this.closed=!0))return this.reset()._reader.return(t);return Pt}next(){if(this.closed)return Pt;let t,{_reader:e}=this;while(t=this._readNextMessageAndValidate())if(t.isSchema())this.reset(t.header());else if(t.isRecordBatch()){this._recordBatchIndex++;let n=t.header(),r=e.readMessageBody(t.bodyLength);return{done:!1,value:this._loadRecordBatch(n,r,t.metadata)}}else if(t.isDictionaryBatch()){this._dictionaryIndex++;let n=t.header(),r=e.readMessageBody(t.bodyLength),a=this._loadDictionaryBatch(n,r);this.dictionaries.set(n.id,a)}if(this.schema&&this._recordBatchIndex===0)return this._recordBatchIndex++,{done:!1,value:new ja(this.schema)};return this.return()}_readNextMessageAndValidate(t){return this._reader.readMessage(t)}};Zo=class Zo extends Dd{constructor(t,e){super(e);this._reader=new ml(this._handle=t)}isAsync(){return!0}isStream(){return!0}[Symbol.asyncIterator](){return this}cancel(){return R(this,void 0,void 0,function*(){if(!this.closed&&(this.closed=!0))yield this.reset()._reader.return(),this._reader=null,this.dictionaries=null})}open(t){return R(this,void 0,void 0,function*(){if(!this.closed){if(this.autoDestroy=s_(this,t),!(this.schema||(this.schema=yield this._reader.readSchema())))yield this.cancel()}return this})}throw(t){return R(this,void 0,void 0,function*(){if(!this.closed&&this.autoDestroy&&(this.closed=!0))return yield this.reset()._reader.throw(t);return Pt})}return(t){return R(this,void 0,void 0,function*(){if(!this.closed&&this.autoDestroy&&(this.closed=!0))return yield this.reset()._reader.return(t);return Pt})}next(){return R(this,void 0,void 0,function*(){if(this.closed)return Pt;let t,{_reader:e}=this;while(t=yield this._readNextMessageAndValidate())if(t.isSchema())yield this.reset(t.header());else if(t.isRecordBatch()){this._recordBatchIndex++;let n=t.header(),r=yield e.readMessageBody(t.bodyLength);return{done:!1,value:this._loadRecordBatch(n,r,t.metadata)}}else if(t.isDictionaryBatch()){this._dictionaryIndex++;let n=t.header(),r=yield e.readMessageBody(t.bodyLength),a=this._loadDictionaryBatch(n,r);this.dictionaries.set(n.id,a)}if(this.schema&&this._recordBatchIndex===0)return this._recordBatchIndex++,{done:!1,value:new ja(this.schema)};return yield this.return()})}_readNextMessageAndValidate(t){return R(this,void 0,void 0,function*(){return yield this._reader.readMessage(t)})}};Bd=class Bd extends Jo{get footer(){return this._footer}get numDictionaries(){return this._footer?this._footer.numDictionaries:0}get numRecordBatches(){return this._footer?this._footer.numRecordBatches:0}constructor(t,e){super(t instanceof kc?t:new kc(t),e)}isSync(){return!0}isFile(){return!0}open(t){if(!this.closed&&!this._footer){this.schema=(this._footer=this._readFooter()).schema;for(let e of this._footer.dictionaryBatches())e&&this._readDictionaryBatch(this._dictionaryIndex++)}return super.open(t)}readRecordBatch(t){var e;if(this.closed)return null;if(!this._footer)this.open();let n=(e=this._footer)===null||e===void 0?void 0:e.getRecordBatch(t);if(n&&this._handle.seek(n.offset)){let r=this._reader.readMessage(pt.RecordBatch);if(r===null||r===void 0?void 0:r.isRecordBatch()){let a=r.header(),o=this._reader.readMessageBody(r.bodyLength);return this._loadRecordBatch(a,o,r.metadata)}}return null}_readDictionaryBatch(t){var e;let n=(e=this._footer)===null||e===void 0?void 0:e.getDictionaryBatch(t);if(n&&this._handle.seek(n.offset)){let r=this._reader.readMessage(pt.DictionaryBatch);if(r===null||r===void 0?void 0:r.isDictionaryBatch()){let a=r.header(),o=this._reader.readMessageBody(r.bodyLength),s=this._loadDictionaryBatch(a,o);this.dictionaries.set(a.id,s)}}}_readFooter(){let{_handle:t}=this,e=t.size-Td,n=t.readInt32(e),r=t.readAt(e-n,n);return Ii.decode(r)}_readNextMessageAndValidate(t){var e;if(!this._footer)this.open();if(this._footer&&this._recordBatchIndexsuper.open}});return R(this,void 0,void 0,function*(){if(!this.closed&&!this._footer){this.schema=(this._footer=yield this._readFooter()).schema;for(let n of this._footer.dictionaryBatches())n&&(yield this._readDictionaryBatch(this._dictionaryIndex++))}return yield e.open.call(this,t)})}readRecordBatch(t){return R(this,void 0,void 0,function*(){var e;if(this.closed)return null;if(!this._footer)yield this.open();let n=(e=this._footer)===null||e===void 0?void 0:e.getRecordBatch(t);if(n&&(yield this._handle.seek(n.offset))){let r=yield this._reader.readMessage(pt.RecordBatch);if(r===null||r===void 0?void 0:r.isRecordBatch()){let a=r.header(),o=yield this._reader.readMessageBody(r.bodyLength);return this._loadRecordBatch(a,o,r.metadata)}}return null})}_readDictionaryBatch(t){return R(this,void 0,void 0,function*(){var e;let n=(e=this._footer)===null||e===void 0?void 0:e.getDictionaryBatch(t);if(n&&(yield this._handle.seek(n.offset))){let r=yield this._reader.readMessage(pt.DictionaryBatch);if(r===null||r===void 0?void 0:r.isDictionaryBatch()){let a=r.header(),o=yield this._reader.readMessageBody(r.bodyLength),s=this._loadDictionaryBatch(a,o);this.dictionaries.set(a.id,s)}}})}_readFooter(){return R(this,void 0,void 0,function*(){let{_handle:t}=this;t._pending&&(yield t._pending);let e=t.size-Td,n=yield t.readInt32(e),r=yield t.readAt(e-n,n);return Ii.decode(r)})}_readNextMessageAndValidate(t){return R(this,void 0,void 0,function*(){if(!this._footer)yield this.open();if(this._footer&&this._recordBatchIndex{let _=n.typeIds[p],g=c[_],w=l[_];return f.slice(g,Math.min(r,w))}))}return this}function gS(t){let e;if(t.nullCount>=t.length)return Sn.call(this,new Uint8Array(t.length+7>>3));else if((e=t.values)instanceof Uint8Array)return Sn.call(this,Gi(t.offset,t.length,e));return Sn.call(this,Wi(t.values))}function ti(t){return Sn.call(this,t.values.subarray(0,t.length*t.stride))}function Sl(t){let{length:e,values:n,valueOffsets:r}=t,a=gt(r[0]),o=gt(r[e]),s=Math.min(o-a,n.byteLength-a);return Sn.call(this,Qs(-a,e+1,r)),Sn.call(this,n.subarray(a,a+s)),this}function c_(t){let{offset:e,length:n,stride:r,values:a,variadicBuffers:o=[]}=t;if(!a)throw Error("BinaryView data is missing view buffer");let s=e*r,c=s+n*r;Sn.call(this,a.subarray(s,c));for(let l of o)Sn.call(this,l);return this._variadicBufferCounts.push(o.length),this}function Rd(t){let{length:e,valueOffsets:n}=t;if(n){let{[0]:r,[e]:a}=n;return Sn.call(this,Qs(-r,e+1,n)),this.visit(t.children[0].slice(r,a-r))}return this.visit(t.children[0])}function Cd(t){return this.visitMany(t.type.children.map((e,n)=>t.children[n]).filter(Boolean))[0]}var qt;var l_=b(()=>{je();ir();ne();Bi();oe();Na();Xi();Ft();qt=class qt extends K{static assemble(...t){let e=(r)=>r.flatMap((a)=>Array.isArray(a)?e(a):a instanceof rt?a.data.children:a.data),n=new qt;return n.visitMany(e(t)),n}constructor(){super();this._byteLength=0,this._nodes=[],this._buffers=[],this._bufferRegions=[],this._variadicBufferCounts=[]}visit(t){if(t instanceof O)return this.visitMany(t.data),this;let{type:e}=t;if(!S.isDictionary(e)){let{length:n}=t;if(n>2147483647)throw RangeError("Cannot write arrays larger than 2^31 - 1 in length");if(S.isUnion(e))this.nodes.push(new or(n,0));else{let{nullCount:r}=t;if(!S.isNull(e))Sn.call(this,r<=0?new Uint8Array(0):Gi(t.offset,n,t.nullBitmap));this.nodes.push(new or(n,r))}}return super.visit(t)}visitNull(t){return this}visitDictionary(t){return this.visit(t.clone(t.type.indices))}get nodes(){return this._nodes}get buffers(){return this._buffers}get byteLength(){return this._byteLength}get bufferRegions(){return this._bufferRegions}get variadicBufferCounts(){return this._variadicBufferCounts}};qt.prototype.visitBool=gS;qt.prototype.visitInt=ti;qt.prototype.visitFloat=ti;qt.prototype.visitUtf8=Sl;qt.prototype.visitLargeUtf8=Sl;qt.prototype.visitUtf8View=c_;qt.prototype.visitBinary=Sl;qt.prototype.visitLargeBinary=Sl;qt.prototype.visitBinaryView=c_;qt.prototype.visitFixedSizeBinary=ti;qt.prototype.visitDate=ti;qt.prototype.visitTimestamp=ti;qt.prototype.visitTime=ti;qt.prototype.visitDecimal=ti;qt.prototype.visitList=Rd;qt.prototype.visitStruct=Cd;qt.prototype.visitUnion=_S;qt.prototype.visitInterval=ti;qt.prototype.visitDuration=ti;qt.prototype.visitFixedSizeList=Rd;qt.prototype.visitMap=Rd});function Od(t,e){let n=e;if(e instanceof Xt)n=e.batches,t.reset(void 0,e.schema);for(let r of n)t.write(r);return t.finish()}function Nd(t,e){return R(this,void 0,void 0,function*(){var n,r,a,o,s,c,l;try{for(n=!0,r=Kn(e);a=yield r.next(),o=a.done,!o;n=!0){l=a.value,n=!1;let f=l;t.write(f)}}catch(f){s={error:f}}finally{try{if(!n&&!o&&(c=r.return))yield c.call(r)}finally{if(s)throw s.error}}return t.finish()})}var ia,an,Ka;var Fd=b(()=>{un();hl();vd();je();Xi();Xi();Lf();ne();Wo();Ji();l_();oe();Bi();Vo();Io();Ed();ht();ia=class ia extends Pa{static throughNode(t){throw Error('"throughNode" not available in this environment')}static throughDOM(t,e){throw Error('"throughDOM" not available in this environment')}constructor(t){super();if(this._position=0,this._started=!1,this._compression=null,this._sink=new Gr,this._schema=null,this._dictionaryBlocks=[],this._recordBatchBlocks=[],this._seenDictionaries=new Map,this._dictionaryDeltaOffsets=new Map,Ve(t)||(t={autoDestroy:!0,writeLegacyIpcFormat:!1,compressionType:null}),this._autoDestroy=typeof t.autoDestroy==="boolean"?t.autoDestroy:!0,this._writeLegacyIpcFormat=typeof t.writeLegacyIpcFormat==="boolean"?t.writeLegacyIpcFormat:!1,t.compressionType!=null){if(this._writeLegacyIpcFormat)throw Error("Legacy IPC format does not support columnar compression. Use modern IPC format (writeLegacyIpcFormat=false).");if(Object.values(qe).includes(t.compressionType))this._compression=new La(t.compressionType);else{let e=Object.values(qe).filter((n)=>typeof n==="string");throw Error(`Unsupported compressionType: ${t.compressionType} Available types: ${e.join(", ")}`)}}else this._compression=null}toString(t=!1){return this._sink.toString(t)}toUint8Array(t=!1){return this._sink.toUint8Array(t)}writeAll(t){if(Gn(t))return t.then((e)=>this.writeAll(e));else if(En(t))return Nd(this,t);return Od(this,t)}get closed(){return this._sink.closed}[Symbol.asyncIterator](){return this._sink[Symbol.asyncIterator]()}toDOMStream(t){return this._sink.toDOMStream(t)}toNodeStream(t){return this._sink.toNodeStream(t)}close(){return this.reset()._sink.close()}abort(t){return this.reset()._sink.abort(t)}finish(){return this._autoDestroy?this.close():this.reset(this._sink,this._schema),this}reset(t=this._sink,e=null){if(t===this._sink||t instanceof Gr)this._sink=t;else if(this._sink=new Gr,t&&eh(t))this.toDOMStream({type:"bytes"}).pipeTo(t);else if(t&&nh(t))this.toNodeStream({objectMode:!1}).pipe(t);if(this._started&&this._schema)this._writeFooter(this._schema);if(this._started=!1,this._dictionaryBlocks=[],this._recordBatchBlocks=[],this._seenDictionaries=new Map,this._dictionaryDeltaOffsets=new Map,!e||!ea(e,this._schema))if(e==null)this._position=0,this._schema=null;else this._started=!0,this._schema=e,this._writeSchema(e);return this}write(t){let e=null;if(!this._sink)throw Error("RecordBatchWriter is closed");else if(t==null)return this.finish()&&void 0;else if(t instanceof Xt&&!(e=t.schema))return this.finish()&&void 0;else if(t instanceof rt&&!(e=t.schema))return this.finish()&&void 0;if(e&&!ea(e,this._schema)){if(this._started&&this._autoDestroy)return this.close();this.reset(this._sink,e)}if(t instanceof rt){if(!(t instanceof ja))this._writeRecordBatch(t)}else if(t instanceof Xt)this.writeAll(t.batches);else if(Wn(t))this.writeAll(t)}_writeMessage(t,e=8){let n=e-1,r=we.encode(t),a=r.byteLength,o=!this._writeLegacyIpcFormat?8:4,s=a+o+n&~n,c=s-a-o;if(t.headerType===pt.RecordBatch)this._recordBatchBlocks.push(new Kr(s,t.bodyLength,this._position));else if(t.headerType===pt.DictionaryBatch)this._dictionaryBlocks.push(new Kr(s,t.bodyLength,this._position));if(!this._writeLegacyIpcFormat)this._write(Int32Array.of(-1));if(this._write(Int32Array.of(s-o)),a>0)this._write(r);return this._writePadding(c)}_write(t){if(this._started){let e=L(t);if(e&&e.byteLength>0)this._sink.write(e),this._position+=e.byteLength}return this}_writeSchema(t){return this._writeMessage(we.from(t))}_writeFooter(t){return this._writeLegacyIpcFormat?this._write(Int32Array.of(0)):this._write(Int32Array.of(-1,0))}_writeMagic(){return this._write($a)}_writePadding(t){return t>0?this._write(new Uint8Array(t)):this}_writeRecordBatch(t){let{byteLength:e,nodes:n,bufferRegions:r,buffers:a,variadicBufferCounts:o}=this._assembleRecordBatch(t),s=new le(t.numRows,n,r,this._compression,o,t.metadata),c=we.from(s,e);return this._writeDictionaries(t)._writeMessage(c)._writeBodyBuffers(a)}_assembleRecordBatch(t){let{byteLength:e,nodes:n,bufferRegions:r,buffers:a,variadicBufferCounts:o}=qt.assemble(t);if(this._compression!=null)({byteLength:e,bufferRegions:r,buffers:a}=this._compressBodyBuffers(a));return{byteLength:e,nodes:n,bufferRegions:r,buffers:a,variadicBufferCounts:o}}_compressBodyBuffers(t){let e=Ya.get(this._compression.type);if(!(e===null||e===void 0?void 0:e.encode)||typeof e.encode!=="function")throw Error(`Codec for compression type "${qe[this._compression.type]}" has invalid encode method`);let n=0,r=[],a=[];for(let s of t){let c=L(s);if(c.length===0){r.push(new Uint8Array(0),new Uint8Array(0)),a.push(new Qt(n,0));continue}let l=e.encode(c),f=l.length0)this._writePadding(o)}return this}_writeDictionaries(t){var e,n;for(let[r,a]of t.dictionaries){let o=(e=a===null||a===void 0?void 0:a.data)!==null&&e!==void 0?e:[],s=this._seenDictionaries.get(r),c=(n=this._dictionaryDeltaOffsets.get(r))!==null&&n!==void 0?n:0;if(!s||s.data[0]!==o[0])for(let[l,f]of o.entries())this._writeDictionaryBatch(f,r,l>0);else if(cn.writeAll(r));else if(En(t))return Nd(n,t);return Od(n,t)}};Ka=class Ka extends ia{static writeAll(t,e){let n=new Ka(e);if(Gn(t))return t.then((r)=>n.writeAll(r));else if(En(t))return Nd(n,t);return Od(n,t)}constructor(t){super(t);this._autoDestroy=!0,this._writeLegacyIpcFormat=!1}_writeSchema(t){return this._writeMagic()._writePadding(2)}_writeDictionaryBatch(t,e,n=!1){if(!n&&this._seenDictionaries.has(e))throw Error("The Arrow File format does not support replacement dictionaries. ");return super._writeDictionaryBatch(t,e,n)}_writeFooter(t){let e=Ii.encode(new Ii(t,Tt.V5,this._recordBatchBlocks,this._dictionaryBlocks));return super._writeFooter(t)._write(e)._write(Int32Array.of(e.byteLength))._writeMagic()}}});function u_(t,e){if(En(t))return wS(t,e);if(Wn(t))return bS(t,e);throw Error("toDOMStream() must be called with an Iterable or AsyncIterable")}function bS(t,e){let n=null,r=(e===null||e===void 0?void 0:e.type)==="bytes"||!1,a=(e===null||e===void 0?void 0:e.highWaterMark)||Math.pow(2,24);return new ReadableStream(Object.assign(Object.assign({},e),{start(s){o(s,n||(n=t[Symbol.iterator]()))},pull(s){n?o(s,n):s.close()},cancel(){(n===null||n===void 0?void 0:n.return)&&n.return(),n=null}}),Object.assign({highWaterMark:r?a:void 0},e));function o(s,c){let l,f=null,p=s.desiredSize||null;while(!(f=c.next(r?p:null)).done){if(ArrayBuffer.isView(f.value)&&(l=L(f.value)))p!=null&&r&&(p=p-l.byteLength+1),f.value=l;if(s.enqueue(f.value),p!=null&&--p<=0)return}s.close()}}function wS(t,e){let n=null,r=(e===null||e===void 0?void 0:e.type)==="bytes"||!1,a=(e===null||e===void 0?void 0:e.highWaterMark)||Math.pow(2,24);return new ReadableStream(Object.assign(Object.assign({},e),{start(s){return R(this,void 0,void 0,function*(){yield o(s,n||(n=t[Symbol.asyncIterator]()))})},pull(s){return R(this,void 0,void 0,function*(){n?yield o(s,n):s.close()})},cancel(){return R(this,void 0,void 0,function*(){(n===null||n===void 0?void 0:n.return)&&(yield n.return()),n=null})}}),Object.assign({highWaterMark:r?a:void 0},e));function o(s,c){return R(this,void 0,void 0,function*(){let l,f=null,p=s.desiredSize||null;while(!(f=yield c.next(r?p:null)).done){if(ArrayBuffer.isView(f.value)&&(l=L(f.value)))p!=null&&r&&(p=p-l.byteLength+1),f.value=l;if(s.enqueue(f.value),p!=null&&--p<=0)return}s.close()})}}var f_=b(()=>{un();oe()});function h_(t){return new m_(t)}class m_{constructor(t){this._numChunks=0,this._finished=!1,this._bufferedSize=0;let{["readableStrategy"]:e,["writableStrategy"]:n,["queueingStrategy"]:r="count"}=t,a=Jp(t,["readableStrategy","writableStrategy","queueingStrategy"]);this._controller=null,this._builder=Qr(a),this._getSize=r!=="bytes"?d_:p_;let{["highWaterMark"]:o=r==="bytes"?Math.pow(2,14):1000}=Object.assign({},e),{["highWaterMark"]:s=r==="bytes"?Math.pow(2,14):1000}=Object.assign({},n);this.readable=new ReadableStream({["cancel"]:()=>{this._builder.clear()},["pull"]:(c)=>{this._maybeFlush(this._builder,this._controller=c)},["start"]:(c)=>{this._maybeFlush(this._builder,this._controller=c)}},{highWaterMark:o,size:r!=="bytes"?d_:p_}),this.writable=new WritableStream({["abort"]:()=>{this._builder.clear()},["write"]:()=>{this._maybeFlush(this._builder,this._controller)},["close"]:()=>{this._maybeFlush(this._builder.finish(),this._controller)}},{highWaterMark:s,size:(c)=>this._writeValueAndReturnChunkSize(c)})}_writeValueAndReturnChunkSize(t){let e=this._bufferedSize;return this._bufferedSize=this._getSize(this._builder.append(t)),this._bufferedSize-e}_maybeFlush(t,e){if(e==null)return;if(this._bufferedSize>=e.desiredSize)++this._numChunks&&this._enqueue(e,t.toVector());if(t.finished){if(t.length>0||this._numChunks===0)++this._numChunks&&this._enqueue(e,t.toVector());if(!this._finished&&(this._finished=!0))this._enqueue(e,null)}}_enqueue(t,e){this._bufferedSize=0,this._controller=null,e==null?t.close():t.enqueue(e)}}var d_=(t)=>{var e;return(e=t===null||t===void 0?void 0:t.length)!==null&&e!==void 0?e:0},p_=(t)=>{var e;return(e=t===null||t===void 0?void 0:t.byteLength)!==null&&e!==void 0?e:0};var y_=b(()=>{un();$c()});function Al(t,e){let n=new Gr,r=null,a=new ReadableStream({cancel(){return R(this,void 0,void 0,function*(){yield n.close()})},start(c){return R(this,void 0,void 0,function*(){yield s(c,r||(r=yield o()))})},pull(c){return R(this,void 0,void 0,function*(){r?yield s(c,r):c.close()})}});return{writable:new WritableStream(n,Object.assign({highWaterMark:Math.pow(2,14)},t)),readable:a};function o(){return R(this,void 0,void 0,function*(){return yield(yield Ot.from(n)).open(e)})}function s(c,l){return R(this,void 0,void 0,function*(){let f=c.desiredSize,p=null;while(!(p=yield l.next()).done)if(c.enqueue(p.value),f!=null&&--f<=0)return;c.close()})}}var __=b(()=>{un();Ji();xl()});function Il(t,e){let n=new this(t),r=new jn(n),a=new ReadableStream({cancel(){return R(this,void 0,void 0,function*(){yield r.cancel()})},pull(s){return R(this,void 0,void 0,function*(){yield o(s)})},start(s){return R(this,void 0,void 0,function*(){yield o(s)})}},Object.assign({highWaterMark:Math.pow(2,14)},e));return{writable:new WritableStream(n,t),readable:a};function o(s){return R(this,void 0,void 0,function*(){let c=null,l=s.desiredSize;while(c=yield r.read(l||null))if(s.enqueue(c),l!=null&&(l-=c.byteLength)<=0)return;s.close()})}}var g_=b(()=>{un();Ji()});var Ud={};mr(Ud,{isArrowData:()=>S_,isArrowDataType:()=>x_,isArrowField:()=>w_,isArrowRecordBatch:()=>I_,isArrowSchema:()=>b_,isArrowTable:()=>T_,isArrowVector:()=>A_});function b_(t){return P.isSchema(t)}function w_(t){return N.isField(t)}function x_(t){return S.isDataType(t)}function S_(t){return tt.isData(t)}function A_(t){return O.isVector(t)}function I_(t){return rt.isRecordBatch(t)}function T_(t){return Xt.isTable(t)}var v_=b(()=>{$e();Ft();zn();je();Bi();hl()});var PS;var C_=b(()=>{ne();zn();Ft();je();$e();$c();xl();Fd();Bi();wf();jf();Na();No();oe();Ra();v_();Wo();PS=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},bf),zf),Nf),xf),ku),Of),mf),Yf),Ud),{compareSchemas:ea,compareFields:qy,compareTypes:fl})});var Ke=b(()=>{wo();$t();xl();Fd();f_();y_();__();g_();C_();Ae.toDOMStream=u_;Lt.throughDOM=h_;Ot.throughDOM=Al;ra.throughDOM=Al;Zr.throughDOM=Al;ia.throughDOM=Il;Ka.throughDOM=Il;an.throughDOM=Il});function TI(t){return typeof t==="object"&&t!==null&&typeof t.buffers==="object"}function bp(t,e){let n=-1;if(ArrayBuffer.isView(t))n=t.byteLength;else if(t instanceof ArrayBuffer)n=t.byteLength;else if(TI(t))for(let r of Object.values(t.buffers)){let a=r?.byteLength;if(typeof a==="number"&&a>n)n=a}else if(typeof t==="string")n=t.length;if(n>2147483640)throw RangeError(`${e} is ${n} bytes; this TypeScript worker can encode at most 2147483640 (2 GiB - 8). Both Arrow backends align buffers with (byteLength + 7) & ~7, and the bitwise & truncates to int32, so a larger value would be sent as a negative bodyLength. The wire and the `+"protocol carry it fine — the limit is the JavaScript Arrow encoders', not vgi-rpc's.")}function bt(t,e,n=!0,r){return new N(t,e,n,r??new Map)}function Oe(t,e){return new P(t,e??new Map)}function cu(t){let e=new an;return e.reset(void 0,t),e.close(),e.toUint8Array(!0)}function Ss(t){let e=Ot.from(t),n=[...e];if(n.length>0)return n[0].schema;if(e.schema)return e.schema;throw Error("Cannot deserialize schema from empty IPC stream")}function ei(t){let e=t,n=new an;return n.reset(void 0,e.schema),n._writeRecordBatch(e),n.close(),n.toUint8Array(!0)}function Sp(t){return[...Ot.from(t)].filter((n)=>n.constructor.name!=="_InternalEmptyPlaceholderRecordBatch")}function vr(t){let e=Ot.from(t),n=[...e];if(n.length===0){let r=e.schema??new P([]),a=new it(r.fields),o=A({type:a,length:0,children:[],nullCount:0});return new rt(r,o)}return n[0]}function lu(t,e){let n=t,r=n.fields.length>0?e[n.fields[0].name]?.length??0:0,a=n.fields.map((c)=>{let l=e[c.name];if(!l)return A({type:c.type,length:r,nullCount:r});for(let f of l)bp(f,c.name);return Ir(l,c.type).data[0]}),o=new it(n.fields),s=A({type:o,length:r,children:a,nullCount:0});return new rt(n,s)}function Ui(t,e){let n=t,r=n.fields.map((s)=>no(s.type)),a=new it(n.fields),o=A({type:a,length:0,children:r,nullCount:0});return new rt(n,o,e)}function no(t){let e={DataType:S,Data:tt};if(e.DataType.isStruct(t)){let n=t.children.map((r)=>no(r.type));return A({type:t,length:0,children:n,nullCount:0})}if(e.DataType.isList(t)){let n=no(t.children[0].type);return A({type:t,length:0,child:n,nullCount:0,valueOffsets:new Int32Array([0])})}if(e.DataType.isFixedSizeList(t)){let n=no(t.children[0].type);return A({type:t,length:0,child:n,nullCount:0})}if(e.DataType.isMap(t)){let n=t.children[0]?.type,r=n?no(n):A({type:new it([]),length:0,children:[],nullCount:0});return A({type:t,length:0,child:r,nullCount:0,valueOffsets:new Int32Array([0])})}if(e.DataType.isUnion(t)){let n=t.children.map((r)=>no(r.type));if(e.DataType.isDenseUnion(t))return A({type:t,length:0,typeIds:new Int8Array(0),valueOffsets:new Int32Array(0),children:n,nullCount:0});return A({type:t,length:0,typeIds:new Int8Array(0),children:n,nullCount:0})}return A({type:t,length:0,nullCount:0})}function io(t,e,n){let r=t,a={DataType:S,Data:tt},o=r.fields.map((l)=>{let f=e[l.name];if(bp(f,l.name),f instanceof a.Data)return f;return Ir([f],l.type).data[0]}),s=new it(r.fields),c=A({type:s,length:1,children:o,nullCount:0});return new rt(r,c,n)}function ur(t,e){let n=t;return new rt(n.schema,n.data,e)}function As(t,e){let n=new an;n.reset(void 0,t);for(let r of e)n._writeRecordBatch(r);return n.close(),n.toUint8Array(!0)}function Ap(t,e){let n=t;if(n.numRows===0)return t;let r=e;if(n.schema.fields.length!==r.fields.length)throw TypeError(`Field count mismatch: expected ${r.fields.length}, got ${n.schema.fields.length}`);for(let l=0;l{let p=n.data.children[f],_=p.type,g=l.type;if(_===g)return p;if(!vI(_,g))return a=!0,p.clone(g);if(xg(_)&&xg(g)){a=!0;let w=n.getChildAt(f),I=[];for(let x=0;xnew jt,wp=()=>new De,he=()=>new Gt,ws=()=>new ze,xp=(t=null)=>new en(E.MICROSECOND,t),xs=(t)=>new ve(t),su=(t)=>new it(t),vI=(t,e)=>{if(t.typeId===e.typeId)return!1;if(t.constructor===e.constructor)return!1;return!0},xg=(t)=>t.typeId===y.Int||t.typeId===y.Float;var Er=b(()=>{Ke();bs={name:"arrow-js",opaquePassthrough:!0}});var Dr=67108864;var Is="vgi_rpc.method",uu="vgi_rpc.protocol",la="vgi_rpc.log_level",Ag="vgi_rpc.log_message",Ip="vgi_rpc.log_extra",Ts="vgi_rpc.request_version",fu="1",Ig="vgi_rpc.request_id",Tg="vgi_rpc.protocol_version",ni="vgi_rpc.stream_state#b64",vs="vgi_rpc.call_state#b64",vg="vgi_rpc.cancel",du="vgi_rpc.location",Tp="vgi_rpc.location.sha256",Eg="vgi_rpc.location.fetch_ms",Dg="vgi_rpc.location.source",Bg="vgi_rpc.error_kind",Cg="vgi_rpc.error_code",Rg="vgi_rpc.error_details";function Ng(t){return typeof t==="string"&&DI.has(t)}function Fg(t){return Ng(t)?t:"UNKNOWN"}function Ug(t){return{"@type":"vgi_rpc.RetryInfo",retry_delay_seconds:t}}function Qe(t,e){let n=t[e]??"";if(typeof n!=="string")throw new ao(`'${e}' must be a string`);return n}function pu(t,e){let n=t[e]??[];if(!Array.isArray(n)||!n.every(hu))throw new ao(`'${e}' must be an array of objects`);return n}function hu(t){return typeof t==="object"&&t!==null&&!Array.isArray(t)}function vp(t){if(!hu(t))return null;let e=t["@type"];if(typeof e!=="string"||!Object.hasOwn(Og,e))return null;try{return Og[e](t)}catch(n){if(n instanceof ao)return null;throw n}}function Mg(t){if(t==null)return[];let e;try{e=JSON.parse(t)}catch{return[]}return Ep(e)}function Ep(t){if(!Array.isArray(t))return[];return t.filter(hu)}function Lg(t,e=[]){let n=Ng(t)?t:"UNKNOWN";if(n==="UNAVAILABLE")return!0;if(n==="RESOURCE_EXHAUSTED")return e.some((r)=>vp(r)?.["@type"]==="vgi_rpc.RetryInfo");return!1}var EI,DI,ao,Og;var Dp=b(()=>{EI=["CANCELLED","UNKNOWN","INVALID_ARGUMENT","DEADLINE_EXCEEDED","NOT_FOUND","ALREADY_EXISTS","PERMISSION_DENIED","RESOURCE_EXHAUSTED","FAILED_PRECONDITION","ABORTED","OUT_OF_RANGE","UNIMPLEMENTED","INTERNAL","UNAVAILABLE","DATA_LOSS","UNAUTHENTICATED"],DI=new Set(EI);ao=class ao extends Error{};Og={"vgi_rpc.ErrorInfo":(t)=>{let e=t.metadata??{};if(!hu(e)||!Object.values(e).every((n)=>typeof n==="string"))throw new ao("'metadata' must be an object of strings");return{"@type":"vgi_rpc.ErrorInfo",metadata:{...e}}},"vgi_rpc.RetryInfo":(t)=>{let e=t.retry_delay_seconds;if(typeof e!=="number"||!Number.isFinite(e)||e<0)throw new ao("'retry_delay_seconds' must be a finite, non-negative number");return Ug(e)},"vgi_rpc.BadRequest":(t)=>({"@type":"vgi_rpc.BadRequest",field_violations:pu(t,"field_violations").map((e)=>({field:Qe(e,"field"),description:Qe(e,"description")}))}),"vgi_rpc.PreconditionFailure":(t)=>({"@type":"vgi_rpc.PreconditionFailure",violations:pu(t,"violations").map((e)=>({type:Qe(e,"type"),subject:Qe(e,"subject"),description:Qe(e,"description")}))}),"vgi_rpc.QuotaFailure":(t)=>({"@type":"vgi_rpc.QuotaFailure",violations:pu(t,"violations").map((e)=>({subject:Qe(e,"subject"),description:Qe(e,"description")}))}),"vgi_rpc.ResourceInfo":(t)=>({"@type":"vgi_rpc.ResourceInfo",resource_type:Qe(t,"resource_type"),resource_name:Qe(t,"resource_name"),owner:Qe(t,"owner"),description:Qe(t,"description")}),"vgi_rpc.Help":(t)=>({"@type":"vgi_rpc.Help",links:pu(t,"links").map((e)=>({description:Qe(e,"description"),url:Qe(e,"url")}))}),"vgi_rpc.LocalizedMessage":(t)=>({"@type":"vgi_rpc.LocalizedMessage",locale:Qe(t,"locale"),message:Qe(t,"message")})}});var k;var Br=b(()=>{Dp();k=class k extends Error{errorType;errorMessage;remoteTraceback;errorCode;errorKind;errorDetails;requestId;constructor(t,e,n,r={}){super(`${t}: ${e}`);this.errorType=t,this.errorMessage=e,this.remoteTraceback=n,this.name="RpcError",this.errorCode=r.errorCode??"",this.errorKind=r.errorKind??"",this.errorDetails=[...r.errorDetails??[]],this.requestId=r.requestId??""}get code(){return Fg(this.errorCode)}isRetryable(){return Lg(this.code,this.errorDetails)}details(){let t=[];for(let e of this.errorDetails){let n=vp(e);if(n)t.push(n)}return t}detail(t){return this.details().find((e)=>e["@type"]===t)??null}errorInfo(){return this.detail("vgi_rpc.ErrorInfo")}retryInfo(){return this.detail("vgi_rpc.RetryInfo")}badRequest(){return this.detail("vgi_rpc.BadRequest")}preconditionFailure(){return this.detail("vgi_rpc.PreconditionFailure")}quotaFailure(){return this.detail("vgi_rpc.QuotaFailure")}resourceInfo(){return this.detail("vgi_rpc.ResourceInfo")}help(){return this.detail("vgi_rpc.Help")}localizedMessage(){return this.detail("vgi_rpc.LocalizedMessage")}}});var oo=b(()=>{Er()});function CI(t,e){let n=(a,o)=>{let s=t.get(a);if(s!==void 0)return s;let c=e?.[o];return typeof c==="string"?c:""},r=t.get(Rg);return{errorCode:n(Cg,"error_code"),errorKind:n(Bg,"error_kind"),errorDetails:r!==void 0?Mg(r):Ep(e?.error_details),requestId:t.get(Ig)??""}}function me(t,e){let n=t.metadata;if(!n)return!1;let r=n.get(la);if(!r)return!1;let a=n.get(Ag)??"";if(r==="EXCEPTION"){let o=n.get(Ip),s="RpcError",c=a,l="",f;if(o)try{let p=JSON.parse(o);if(p!==null&&typeof p==="object"&&!Array.isArray(p))f=p,s=typeof f.exception_type==="string"?f.exception_type:"RpcError",c=typeof f.exception_message==="string"?f.exception_message:a,l=typeof f.traceback==="string"?f.traceback:""}catch{}throw new k(s,c,l,CI(n,f))}if(e){let o=n.get(Ip),s;if(o)try{s=JSON.parse(o)}catch{}e({level:r,message:a,extra:s})}return!0}var Bp=b(()=>{Dp();Br()});function qg(t,e){var n=[],r=+!e,a=0,o=0;for(;t.length;){var s=MI(t,r||e);if(typeof s=="object"){if(r){if(e=null,s.w.length==s.u)n.push(e=s.w),o+=s.u}else n.push(e),s.e=0;for(;!s.l;){var c=$I(t,s,e);if(!c)ie(5);if(e)s.e=s.y;else n.push(c),o+=c.length,NI(s.w,0,c.length),s.w.set(c,s.w.length-c.length)}a=s.b+s.c*4}else a=s;t=t.subarray(a)}return HI(n,o)}var RI,Jt,mu,OI,yu,Cp=function(t,e,n){if(Jt.prototype.slice)return Jt.prototype.slice.call(t,e,n);if(e==null||e<0)e=0;if(n==null||n>t.length)n=t.length;var r=new Jt(n-e);return r.set(t.subarray(e,n)),r},Ds=function(t,e,n,r){if(Jt.prototype.fill)return Jt.prototype.fill.call(t,e,n,r);if(n==null||n<0)n=0;if(r==null||r>t.length)r=t.length;for(;nt.length)r=t.length;while(n>>0},MI=function(t,e){var n=t[0]|t[1]<<8|t[2]<<16;if(n==3126568&&t[3]==253){var r=t[4],a=r>>5&1,o=r>>2&1,s=r&3,c=r>>6;if(r&8)ie(0);var l=6-a,f=s==3?4:s,p=kg(t,l,f);l+=f;var _=c?1<>3);w=I+(I>>3)*(t[5]&7)}if(w>2145386496)ie(1);var x=new Jt((e==1?g||w:e?0:w)+12);return x[0]=1,x[4]=4,x[8]=8,{b:l+_,y:0,l:0,d:p,w:e&&e!=1?e:x.subarray(12),e:w,o:new yu(x.buffer,0,3),u:g,c:o,m:Math.min(131072,w)}}else if((n>>4|t[3]<<20)==25481893)return UI(t,4)+8;ie(0)},ua=function(t){var e=0;for(;1<n)ie(3);var o=1<0){var Y=ua(s+1),M=r>>3,nt=(1<>(r&7)&nt,Z=(1<Z)lt-=Ct;if(g[++c]=--lt,lt==-1)s+=lt,C[--p]=c;else s-=lt;if(!lt)do{var sn=r>>3;l=(t[sn]|t[sn+1]<<8)>>(r&7)&3,r+=2,c+=l}while(l==3)}if(c>255||s)ie(0);var Zt=0,ye=(o>>1)+(o>>3)+3,vn=o-1;for(var _e=0;_e<=c;++_e){var mt=g[_e];if(mt<1){w[_e]=-mt;continue}for(f=0;f=p)}}if(Zt)ie(0);for(f=0;f>3,{b:a,s:C,n:D,t:I}]},LI=function(t,e){var n=0,r=-1,a=new Jt(292),o=t[e],s=a.subarray(0,256),c=a.subarray(256,268),l=new mu(a.buffer,268);if(o<128){var f=Bs(t,e+1,6),p=f[0],_=f[1];e+=o;var g=p<<3,w=t[e];if(!w)ie(0);var I=0,x=0,C=_.b,D=C,Y=(++e<<3)-8+ua(w);for(;;){if(Y-=C,Y>3;if(I+=(t[M]|t[M+1]<<8)>>(Y&7)&(1<>3,x+=(t[M]|t[M+1]<<8)>>(Y&7)&(1<255)ie(0)}else{r=o-127;for(;n>4,s[n+1]=nt&15}++e}var lt=0;for(n=0;n11)ie(0);lt+=Z&&1<0;--n){var _e=l[n];Ds(vn,n,_e,l[n-1]=_e+c[n]*(1<c&&_>3,w=(t[g]|t[g+1]<<8|t[g+2]<<16)>>(p&7);l=(l<>2,s=o<<1,c=o+s;Es(t.subarray(r,r+=t[0]|t[1]<<8),e.subarray(0,o),n),Es(t.subarray(r,r+=t[2]|t[3]<<8),e.subarray(o,s),n),Es(t.subarray(r,r+=t[4]|t[5]<<8),e.subarray(s,c),n),Es(t.subarray(r),e.subarray(c),n)},$I=function(t,e,n){var r,a=e.b,o=t[a],s=o>>1&3;e.l=o&1;var c=o>>3|t[a+1]<<5|t[a+2]<<13,l=(a+=3)+c;if(s==1){if(a>=t.length)return;if(e.b=a+1,n)return Ds(n,t[a],e.y,e.y+=c),n;return Ds(new Jt(c),t[a])}if(l>t.length)return;if(s==0){if(e.b=l,n)return n.set(t.subarray(a,l),e.y),e.y+=c,n;return Cp(t,a,l)}if(s==2){var f=t[a],p=f&3,_=f>>2&3,g=f>>4,w=0,I=0;if(p<2)if(_&1)g|=t[++a]<<4|(_&2&&t[++a]<<12);else g=f>>3;else if(I=_,_<2)g|=(t[++a]&63)<<4,w=t[a]>>6|t[++a]<<2;else if(_==2)g|=t[++a]<<4|(t[++a]&3)<<12,w=t[a]>>2|t[++a]<<6;else g|=t[++a]<<4|(t[++a]&63)<<12,w=t[a]>>6|t[++a]<<2|t[++a]<<10;++a;var x=n?n.subarray(e.y,e.y+e.m):new Jt(e.m),C=x.length-g;if(p==0)x.set(t.subarray(a,a+=g),C);else if(p==1)Ds(x,t[a++],C);else{var D=e.h;if(p==2){var Y=LI(t,a);w+=a-(a=Y[0]),e.h=D=Y[1]}else if(!D)ie(0);(I?jI:Es)(t.subarray(a,a+=w),x.subarray(C),D)}var M=t[a++];if(M){if(M==255)M=(t[a++]|t[a++]<<8)+32512;else if(M>127)M=M-128<<8|t[a++];var nt=t[a++];if(nt&3)ie(0);var lt=[kI,VI,PI];for(var Z=2;Z>-1;--Z){var Ct=nt>>(Z<<1)+2&3;if(Ct==1){var xe=new Jt([0,0,t[a++]]);lt[Z]={s:xe.subarray(2,3),n:xe.subarray(0,1),t:new mu(xe.buffer,0,1),b:0}}else if(Ct==2)r=Bs(t,a,9-(Z&1)),a=r[0],lt[Z]=r[1];else if(Ct==3){if(!e.t)ie(0);lt[Z]=e.t[Z]}}var sn=e.t=lt,Zt=sn[0],ye=sn[1],vn=sn[2],_e=t[l-1];if(!_e)ie(0);var mt=(l<<3)-8+ua(_e)-vn.b,dt=mt>>3,Se=0,ai=(t[dt]|t[dt+1]<<8)>>(mt&7)&(1<>3;var oi=(t[dt]|t[dt+1]<<8)>>(mt&7)&(1<>3;var Ur=(t[dt]|t[dt+1]<<8)>>(mt&7)&(1<>3;var ot=1<>>(mt&7)&ot-1);dt=(mt-=Op[qs])>>3;var Ut=zI[qs]+((t[dt]|t[dt+1]<<8|t[dt+2]<<16)>>(mt&7)&(1<>3;var Mt=qI[ha]+((t[dt]|t[dt+1]<<8|t[dt+2]<<16)>>(mt&7)&(1<>3,ai=vn.t[ai]+((t[dt]|t[dt+1]<<8)>>(mt&7)&(1<>3,Ur=Zt.t[Ur]+((t[dt]|t[dt+1]<<8)>>(mt&7)&(1<>3,oi=ye.t[oi]+((t[dt]|t[dt+1]<<8)>>(mt&7)&(1<3)e.o[2]=e.o[1],e.o[1]=e.o[0],e.o[0]=At-=3;else{var Ht=At-(Mt!=0);if(Ht){if(At=Ht==3?e.o[0]-1:e.o[Ht],Ht>1)e.o[2]=e.o[1];e.o[1]=e.o[0],e.o[0]=At}else At=e.o[0]}for(var Z=0;ZUt)te=Ut;for(var Z=0;Z{RI=ArrayBuffer,Jt=Uint8Array,mu=Uint16Array,OI=Int16Array,yu=Int32Array,FI=["invalid zstd data","window size too large (>2046MB)","invalid block type","FSE accuracy too high","match distance too far back","unexpected EOF"],PI=Bs(new Jt([81,16,99,140,49,198,24,99,12,33,196,24,99,102,102,134,70,146,4]),0,6)[1],kI=Bs(new Jt([33,20,196,24,99,140,33,132,16,66,8,33,132,16,66,8,33,68,68,68,68,68,68,68,68,36,9]),0,6)[1],VI=Bs(new Jt([32,132,16,66,102,70,68,68,68,68,36,73,2]),0,5)[1],Rp=new Jt(new yu([0,0,0,0,16843009,50528770,134678020,202050057,269422093]).buffer,0,36),qI=Vg(Rp,0),Op=new Jt(new yu([0,0,0,0,0,0,0,0,16843009,50528770,117769220,185207048,252579084,16]).buffer,0,53),zI=Vg(Op,3)});function $g(){let t=import.meta.require??globalThis.require??null;if(!t)return null;try{return t(YI)}catch{return null}}async function Hg(t,e){if(jg)return new Uint8Array(Bun.zstdCompressSync(t,{level:e}));let n=$g(),r=n?.zstdCompressSync;if(typeof r!=="function")throw Error("zstd compression is not available in this runtime. Requires Bun or Node.js >= 22.15 / Deno >= 2.6.9. (workerd has no native zstd encoder; fzstd is decompress-only.)");return new Uint8Array(r(t,{params:{[n.constants.ZSTD_c_compressionLevel]:e}}))}async function Np(t,e){if(e!=null){let r=KI(t);if(r!==null&&r>e)throw Error(`zstd decompressed size (${r}) would exceed cap (${e})`)}let n;if(jg)n=new Uint8Array(Bun.zstdDecompressSync(t));else{let a=$g()?.zstdDecompressSync;if(typeof a==="function")n=new Uint8Array(a(t));else{let o=qg(t);n=new Uint8Array(o.byteLength),n.set(o)}}if(e!=null&&n.byteLength>e)throw Error(`zstd decompressed size (${n.byteLength}) exceeds cap (${e})`);return n}function KI(t){if(t.length<6)return null;if(t[0]!==40||t[1]!==181||t[2]!==47||t[3]!==253)return null;let e=t[4],n=e>>6&3,r=(e>>5&1)===1,a=e&3,o=n===0?r?1:0:n===1?2:n===2?4:8;if(o===0)return null;let s=r?0:1,c=a===0?0:a===1?1:a===2?2:4,l=5+s+c;if(t.lengthBigInt(Number.MAX_SAFE_INTEGER))return Number.MAX_SAFE_INTEGER;return Number(f)}var YI="node:zlib",jg;var Fp=b(()=>{zg();jg=typeof globalThis.Bun<"u"});function Yg(t,e){return Ui(t,e)}var Kg=b(()=>{oo()});function Gg(){return typeof performance<"u"?performance.now():Date.now()}function Wg(t){let e=new URL(t);if(e.protocol!=="https:")throw Error(`External location URL must use HTTPS, got "${e.protocol}"`)}function fa(t){try{let e=new URL(t);return e.username="",e.password="",e.search="",e.hash="",e.toString()}catch{return""}}function QI(t){let e=t.maxFetchBytes??GI,n=t.maxDecompressedBytes??Math.min(Number.MAX_SAFE_INTEGER,e*16),r=t.maxRedirects??WI;if(!Number.isSafeInteger(e)||e<0)throw Error("maxFetchBytes must be a non-negative safe integer");if(!Number.isSafeInteger(n)||n<0)throw Error("maxDecompressedBytes must be a non-negative safe integer");if(!Number.isSafeInteger(r)||r<0)throw Error("maxRedirects must be a non-negative safe integer");return{maxFetchBytes:e,maxDecompressedBytes:n,maxRedirects:r}}async function Qg(t,e,n,r="max_fetch_bytes",a){let o=t.headers.get("Content-Length");if(o!=null){let x=Number(o);if(Number.isFinite(x)&&x>e)throw n.abort(),Error(`External location fetch exceeds ${r} (${x} > ${e})`)}if(!t.body)return new Uint8Array;let s=t.body.getReader(),c=[],l=0,f=e,p=r,_=a==null,g=[];try{while(!0){let{done:x,value:C}=await s.read();if(x)break;if(l+=C.byteLength,!_){for(let Y=0;Y0&&g[0]!==31||g.length>1&&g[1]!==139)f=a,p="max_decompressed_bytes",_=!0;else if(g.length>=2)_=!0}let D=_?f:Math.max(e,a);if(l>D)throw n.abort(),Error(`External location fetch exceeded ${p} (${f} bytes)`);c.push(C)}}finally{try{await s.cancel()}catch{}s.releaseLock()}if(l>f)throw n.abort(),Error(`External location fetch exceeded ${p} (${f} bytes)`);let w=new Uint8Array(l),I=0;for(let x of c)w.set(x,I),I+=x.byteLength;return w}async function XI(t){let e=new ArrayBuffer(t.byteLength);new Uint8Array(e).set(t);let n=await crypto.subtle.digest("SHA-256",e);return Array.from(new Uint8Array(n)).map((r)=>r.toString(16).padStart(2,"0")).join("")}function Ne(t){if(t.numRows!==0)return!1;let e=t.metadata;if(!e)return!1;return e.has(du)&&!e.has(la)}function Xg(t,e,n){let r=new Map;if(r.set(du,e),n)r.set(Tp,n);return Yg(t,r)}async function Hn(t,e,n){if(!e)return t;if(!Ne(t))return t;let r=t.metadata?.get(du);if(!r)return t;let{maxFetchBytes:a,maxDecompressedBytes:o,maxRedirects:s}=QI(e),c=e.urlValidator===null?void 0:e.urlValidator??Wg,l=Gg(),f=r,p,_;for(let D=0;;D++){if(c)try{c(f)}catch(M){let nt=c===Wg&&M instanceof Error?`: ${M.message}`:"";throw Error(`External location URL rejected [url: ${fa(f)}]${nt}`)}_=new AbortController;try{p=await(e.fetch??globalThis.fetch)(f,{redirect:"manual",signal:_.signal,decompress:!1})}catch{throw Error(`External location fetch failed [url: ${fa(f)}]`)}if(![301,302,303,307,308].includes(p.status))break;if(D>=s)throw _.abort(),Error(`External location redirect limit exceeded (${s})`);let Y=p.headers.get("Location");if(!Y)throw _.abort(),Error(`External location redirect had no Location header [url: ${fa(f)}]`);try{f=new URL(Y,f).toString()}catch{throw _.abort(),Error(`External location redirect target is invalid [url: ${fa(f)}]`)}_.abort()}if(!p.ok)throw Error(`External location fetch failed: ${p.status} ${p.statusText} [url: ${fa(f)}]`);let g=p.headers.get("Content-Encoding"),w=await Qg(p,a,_,"max_fetch_bytes",g?.toLowerCase()==="gzip"?o:void 0);if(g==="zstd")try{w=new Uint8Array(await Np(w,o))}catch(D){if(D instanceof Error&&/(?:decompressed size|\bcap\b)/i.test(D.message))throw Error(`External location decompressed body exceeds max_decompressed_bytes (${o})`);throw Error("External location zstd decompression failed")}if(g?.toLowerCase()==="gzip"&&w[0]===31&&w[1]===139){let D=new Uint8Array(w),Y=new Blob([D]).stream().pipeThrough(new DecompressionStream("gzip"));try{w=await Qg(new Response(Y),o,_,"max_decompressed_bytes")}catch(M){if(M instanceof Error&&M.message.includes("max_decompressed_bytes"))throw M;throw Error("External location gzip decompression failed")}}if(w.byteLength>o)throw Error(`External location decompressed body exceeds max_decompressed_bytes (${w.byteLength} > ${o})`);let I=t.metadata?.get(Tp);if(I){let D=await XI(w);if(D!==I)throw Error(`SHA-256 checksum mismatch for ${fa(f)}: expected ${I}, got ${D}`)}e.validateFetchedIpc?.(w);let x=null;for(let D of Sp(w)){if(D.numRows===0&&me(D,n))continue;x??=D}if(x===null||x.numRows===0&&x.schema.fields.length===0)throw Error(`No data batch found in external IPC stream from ${fa(f)}`);let C=new Map(x.metadata??[]);return C.set(Eg,(Gg()-l).toFixed(1)),C.set(Dg,r),ur(x,C)}var GI=268435456,WI=5;var Cs=b(()=>{oo();Bp();Fp();Kg()});function co(t){return t?"zstd, gzip":"gzip"}var JI,HV,_u="Content-Encoding",gu="X-VGI-Content-Encoding",so="X-VGI-Accept-Encoding";var Rs=b(()=>{JI=["zstd","gzip"],HV=[...JI,"identity"]});async function ZI(t,e,n){let r=e.writable.getWriter(),a=e.readable.getReader(),o=new Uint8Array(t.byteLength);o.set(t);let s=(async()=>{await r.write(o),await r.close()})(),c=[],l=0;while(!0){let{value:_,done:g}=await a.read();if(g)break;let w=_;if(l+=w.byteLength,n!=null&&l>n)throw Error(`gzip decompressed size (${l}) exceeds cap (${n})`);c.push(w)}await s;let f=new Uint8Array(l),p=0;for(let _ of c)f.set(_,p),p+=_.byteLength;return f}async function Jg(t,e){return ZI(t,new DecompressionStream("gzip"),e)}function Zg(t,e,n){return`${n?.prefix??""}/${t}/${e}${n?.suffix??""}`}function bu(t,e){return`${e?.prefix??""}/${t}`}function Mi(t,e,n){return`${t}/${e}${n?.suffix??""}`}function Rr(t,e){let n=e.map((r)=>Ap(r,t));return As(t,n)}var Cr="application/vnd.apache.arrow.stream",Up="__upload_url__",tb,WV,wu="VGI-Session",eb="VGI-Session-Accept",nb="VGI-Session-Close",Mp="VGI-Echo-",rb="__session__";var da=b(()=>{oo();tb=Oe([bt("count",wp(),!0)]),WV=Oe([bt("upload_url",he(),!1),bt("download_url",he(),!1),bt("expires_at",xp("UTC"),!1)])});function Lp(t){if(!/^[1-9][0-9]*$/.test(t))throw TypeError("must be a positive decimal integer");let e=Number(t);if(!Number.isSafeInteger(e)||e>ib)throw TypeError(`must not exceed ${ib}`);return e}function tT(t,e){if(t===void 0)return;if(!Number.isSafeInteger(t)||t<=0)throw TypeError(`${e} must be a positive safe integer`);return t}function ab(t){let e=Lp(t);if(e<65536)throw TypeError("must be at least 65536");return e}function Nr(t,e){let n=tT(t,e);if(n!==void 0&&n<65536)throw TypeError(`${e} must be at least 65536`);return n}function fr(...t){let e;for(let n of t)if(n!==void 0&&n>0&&(e===void 0||n{ib=Number.MAX_SAFE_INTEGER});function xu(t,e){return t.get(e)??t.get(e.toLowerCase())}function lb(t,e){let n=xu(t,e);if(n==null)return[];return n.split(",").map((r)=>r.trim()).filter((r)=>r.length>0)}function Os(t,e,n=!1){let r=t.get(e)??t.get(e.toLowerCase());if(r==null)return null;return n?ab(r):Lp(r)}function ub(t){let n=(t.get(ob)??t.get(ob.toLowerCase()))==="true",r=null,a=t.get("Cache-Control")??t.get("cache-control");if(a)for(let c of a.split(",")){let l=c.trim().toLowerCase();if(l.startsWith("max-age=")){let f=Number.parseFloat(l.slice(8));if(Number.isFinite(f))r=Date.now()+f*1000;break}}let s=xu(t,cb)==null?["zstd"]:lb(t,cb).map((c)=>c.toLowerCase());return{maxRequestBytes:Os(t,eT),uploadUrlSupport:n,maxUploadBytes:Os(t,nT),maxResponseBytes:Os(t,rT,!0),acceptMaxResponseBytesSupport:(t.get(sb)??t.get(sb.toLowerCase()))==="true",maxExternalizedResponseBytes:Os(t,iT,!0),externalizationEnabled:xu(t,aT)==="true",supportedEncodings:s,stickyEnabled:xu(t,oT)==="true",stickyDefaultTtl:Os(t,sT),stickyEchoHeaders:lb(t,cT),cacheExpiresAt:r}}function Li(t){let e=ub(t);if(!e.acceptMaxResponseBytesSupport)throw new k("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true on every RPC response","");return e}async function ri(t,e,n,r,a=globalThis.fetch){let o={};if(n)o.Authorization=n;let s=r??Dr;Nr(s,"acceptedMaxResponseBytes"),o[Or]=String(s);let c=await a(`${t}${e}/health`,{method:"HEAD",headers:o});if(!c.ok)throw new k("TransportError",`Capability discovery failed: HTTP ${c.status}`,"");return ub(c.headers)}function fb(t){if(!t)return!1;if(t.cacheExpiresAt==null)return!0;return Date.now(){Br();lo()});function lT(t){if(t.byteLength<4)return!1;if(t[0]===40&&t[1]===181&&t[2]===47&&t[3]===253)return!0;return(t[0]&240)===80&&t[1]===42&&t[2]===77&&t[3]===24}async function Pi(t,e,n=uT){let r=db(t.headers),a=t.headers.get(gu)?.trim().toLowerCase(),o=t.headers.get(_u)?.trim().toLowerCase(),s=r.codec!==null,c=!a&&o!==void 0&&o!==null&&["gzip","deflate","br"].includes(o),l=s?n:e,f=t.headers.get("Content-Length");if(!c&&f!=null&&/^[0-9]+$/.test(f)){let x=Number(f);if(Number.isSafeInteger(x)&&x>l)throw await t.body?.cancel("response limit exceeded"),new k("TransportError",`HTTP response exceeds ${s?"representation safety":"accepted"} limit (${x} > ${l})`,"")}if(!t.body)return new Uint8Array;let p=t.body.getReader(),_=[],g=0;try{while(!0){let{value:x,done:C}=await p.read();if(C)break;if(g+=x.byteLength,g>l)throw await p.cancel("response limit exceeded"),new k("TransportError",`HTTP response exceeds ${s?"representation safety":"accepted"} limit (${g} > ${l})`,"");_.push(x)}}finally{p.releaseLock()}let w=new Uint8Array(g),I=0;for(let x of _)w.set(x,I),I+=x.byteLength;return w}function db(t){let e=t.get(gu)?.trim().toLowerCase();if(e&&e!=="identity")return{codec:e,custom:!0};let n=t.get(_u)?.trim().toLowerCase();if(n==="zstd")return{codec:"zstd",custom:!1};if(n&&n!=="identity"&&!["gzip","deflate","br"].includes(n))return{codec:n,custom:!1};return{codec:null,custom:!1}}async function uo(t,e,n,r){let{codec:a,custom:o}=db(t);if(!a)return e;if(a==="gzip")return new Uint8Array(await Jg(e,r));if(a==="zstd"){if(!o&&!lT(e))return e;if(!n)throw new k("ProtocolError","Server sent a zstd-encoded response but this client has no zstd decoder. Install the optional zstd dependency, or configure the server not to negotiate zstd.","");let s=new Uint8Array(await n(e,r));if(r!=null&&s.byteLength>r)throw new k("TransportError",`Decoded HTTP response exceeds accepted limit (${s.byteLength} > ${r})`,"");return s}throw new k("ProtocolError",`Unsupported response encoding '${a}'${o?` (${gu})`:` (${_u})`}.`,"")}var uT=268435456;var Fs=b(()=>{Br();Rs()});var pb="vgi_rpc.";var hb=()=>{};function Su(t,e){return Error(`Could not decode a '${pa}' ${t}: ${e}. The reply came from a server this client cannot read; check that it hosts a compatible major version of the reflection protocol.`)}function dr(t,e,n){let r=t[e];if(r===void 0||r===null)throw Su(n,`it carries no '${e}', and that field has no default`);return r}function fo(t,e,n){let r=t[e];return r===void 0||r===null?n:r}function Pp(t,e){let n=t[e];if(n===void 0||n===null)return new Uint8Array(0);return n}function gb(t,e,n){let r=t.getChild(e);if(r===null)throw Su(n,`the payload has no '${e}' column`);let a=r.get(0);if(a===void 0||a===null)throw Su(n,`its '${e}' column is null`);return[...a]}function bb(t,e){let n=vr(t);if(n.numRows<1)throw Su(e,"the payload carries no rows");return n}function wb(t){return{protocol:dr(t,"protocol","protocol summary"),protocol_version:dr(t,"protocol_version","protocol summary"),protocol_hash:dr(t,"protocol_hash","protocol summary"),deprecated:fo(t,"deprecated",!1),deprecation_message:fo(t,"deprecation_message",""),features:[...fo(t,"features",[])].map(String)}}function dT(t){return{name:dr(t,"name","method description"),method_type:dr(t,"method_type","method description"),has_return:dr(t,"has_return","method description"),has_header:dr(t,"has_header","method description"),stream_kind:dr(t,"stream_kind","method description"),params_schema_ipc:Pp(t,"params_schema_ipc"),result_schema_ipc:Pp(t,"result_schema_ipc"),header_schema_ipc:Pp(t,"header_schema_ipc"),idempotency:fo(t,"idempotency","unknown"),deprecated:fo(t,"deprecated",!1),deprecation_message:fo(t,"deprecation_message","")}}function xb(t){let e=bb(t,"protocol listing"),n={server_id:e.getChild("server_id")?.get(0),server_version:e.getChild("server_version")?.get(0),request_version:e.getChild("request_version")?.get(0)};return{server_id:dr(n,"server_id","protocol listing"),server_version:dr(n,"server_version","protocol listing"),request_version:dr(n,"request_version","protocol listing"),protocols:gb(e,"protocols","protocol listing").map(wb)}}function Sb(t){let e=bb(t,"service description"),n={};for(let r of e.schema.fields)if(r.name!=="methods")n[r.name]=e.getChild(r.name)?.get(0);return{...wb(n),methods:gb(e,"methods","service description").map(dT)}}var pa="vgi_rpc.Reflection.v1",mb="list_protocols",Au="describe",yb,_b,fT,cq,lq;var Ab=b(()=>{oo();yb=Oe([bt("protocol",he(),!1)]),_b=[bt("protocol",he(),!1),bt("protocol_version",he(),!1),bt("protocol_hash",he(),!1),bt("deprecated",ro(),!1),bt("deprecation_message",he(),!1),bt("features",xs(bt("item",he(),!0)),!1)],fT=[bt("name",he(),!1),bt("method_type",he(),!1),bt("has_return",ro(),!1),bt("has_header",ro(),!1),bt("stream_kind",he(),!1),bt("params_schema_ipc",ws(),!1),bt("result_schema_ipc",ws(),!1),bt("header_schema_ipc",ws(),!1),bt("idempotency",he(),!1),bt("deprecated",ro(),!1),bt("deprecation_message",he(),!1)],cq=Oe([bt("server_id",he(),!1),bt("server_version",he(),!1),bt("request_version",he(),!1),bt("protocols",xs(bt("item",su(_b),!0)),!1)]),lq=Oe([..._b,bt("methods",xs(bt("item",su(fT),!0)),!1)])});function hT(t){let e=t;return typeof e?.read==="function"&&typeof e?.pipe==="function"}function mT(t){return new Proxy(t,{get(e,n){if(n==="read")return(a)=>e.read(typeof a==="number"?Math.min(a,pT):a);let r=Reflect.get(e,n,e);return typeof r==="function"?r.bind(e):r}})}class Iu{useActiveBackend;reader;initialized=!1;streamEnded=!1;constructor(t,e){this.useActiveBackend=e,this.reader=t}static async create(t,e={}){let n=hT(t)?mT(t):t,r=await Ot.from(n);if(await r.open({autoDestroy:!1}),r.closed)throw Error("Input stream closed before first IPC message");return new Iu(r,e.useActiveBackend===!0&&bs.name!=="arrow-js")}schemaForBackend(t){return this.useActiveBackend?Ss(cu(t)):t}batchForBackend(t){return this.useActiveBackend?vr(ei(t)):t}async readStream(){if(this.initialized){if(await this.reader.reset().open(),this.reader.closed)return null}this.initialized=!0;let t=this.reader.schema;if(!t)return null;let e=[];while(!0){let n=await this.reader.next();if(n.done)break;if(n.value.constructor.name==="_InternalEmptyPlaceholderRecordBatch")break;e.push(this.batchForBackend(n.value))}return{schema:this.schemaForBackend(t),batches:e}}async openNextStream(){if(this.initialized){if(await this.reader.reset().open(),this.reader.closed)return null}return this.initialized=!0,this.streamEnded=!1,this.reader.schema?this.schemaForBackend(this.reader.schema):null}async readNextBatch(){if(this.streamEnded)return null;let t=await this.reader.next();if(t.done)return this.streamEnded=!0,null;if(t.value.constructor.name==="_InternalEmptyPlaceholderRecordBatch")return this.streamEnded=!0,null;return this.batchForBackend(t.value)}async cancel(){await this.reader.cancel()}}var pT=67108864;var Ib=b(()=>{Ke();Er();oo()});function Tb(t){if(typeof t==="string")return new Gt;if(typeof t==="boolean")return new jt;if(typeof t==="bigint")return new De;if(typeof t==="number")return new _n;if(t instanceof Uint8Array)return new ze;return new Gt}function kp(t,e){if(e==null)return e;if(S.isInt(t)&&t.bitWidth===64){if(typeof e==="number")return BigInt(e);return e}if(S.isMap(t)){if(e instanceof Map){let r=t.children[0].type.children[1].type,a=new Map;for(let[o,s]of e)a.set(o,kp(r,s));return a}return e}if(S.isList(t)){if(Array.isArray(e)){let n=t.children[0].type;return e.map((r)=>kp(n,r))}return e}return e}function ki(t,e,n,r){let a=new Map;if(a.set(Is,n),r?.protocol)a.set(uu,r.protocol);if(a.set(Ts,fu),r?.protocolVersion)a.set(Tg,r.protocolVersion);if(t.fields.length===0){let c=Ui(t,a);return Rr(t,[c])}let o={};for(let c of t.fields){let l=e[c.name];o[c.name]=l===void 0?null:kp(c.type,l)}let s=io(t,o,a);return Rr(t,[s])}async function Fr(t){let e=await Ot.from(t);await e.open();let n=e.schema;if(!n)throw new k("ProtocolError","Empty IPC stream: no schema","");let r=e.readAll();return{schema:n,batches:r}}function ii(t){let e=[];for(let n=0;n=BigInt(Number.MIN_SAFE_INTEGER)&&s<=BigInt(Number.MAX_SAFE_INTEGER))s=Number(s)}r[o.name]=s}e.push(r)}return e}async function vb(t){let e=new ReadableStream({start(n){n.enqueue(t),n.close()}});return Iu.create(e)}var Us=b(()=>{Ke();Er();Br();da();Ib();Bp()});function Vp(t){if(t.length===0)return new P([]);return Ss(t)}function yT(t){let e=t.method_type==="stream"?"stream":"unary",n={name:t.name,type:e,hasReturn:t.has_return,paramsSchema:Vp(t.params_schema_ipc),resultSchema:Vp(t.result_schema_ipc)};if(e==="stream")n.streamKind=t.stream_kind;if(t.has_header)n.headerSchema=Vp(t.header_schema_ipc);return n}function _T(t,e){return{protocolName:t.protocol,protocolVersion:t.protocol_version,protocolHash:t.protocol_hash,hostedProtocols:e?e.protocols.map((n)=>n.protocol):[],methods:t.methods.map(yT),serverId:e?.server_id,requestVersion:e?.request_version}}function gT(t){let e=t.protocols.find((n)=>!n.protocol.startsWith(pb));if(!e)throw new k("ProtocolError",`Server ${t.server_id} hosts no application protocol: it offers only [${t.protocols.map((n)=>n.protocol).join(", ")}]. Name a protocol explicitly to describe one of those.`,"");return e.protocol}async function bT(t,e,n){let r=null;for(let s of t){if(s.numRows===0){if(Ne(s)){r=await Hn(s,n,e);continue}me(s,e);continue}r=s}if(!r)throw new k("ProtocolError",`Empty '${pa}' response`,"");let o=(r.getChild("result")??r.getChildAt(0))?.get(0);if(!(o instanceof Uint8Array))throw new k("ProtocolError",`A '${pa}' reply carried no 'result' column of bytes`,"");return o}function wT(t,e){if(t===Au)return ki(yb,{protocol:e},Au,{protocol:pa});return ki(Oe([]),{},t,{protocol:pa})}async function qp(t,e){let n=t.replace(/\/+$/,""),r=e?.prefix??"",a={"Content-Type":Cr};if(e?.authorization)a.Authorization=e.authorization;let o=e?.compressionLevel,s=e?.compressFn,c=e?.decompressFn;if(o!=null&&s)a["Content-Encoding"]="zstd";if(o!=null&&c)a["Accept-Encoding"]="zstd";a[so]=co(c!=null);let l=e?.acceptedMaxResponseBytes??Dr;Nr(l,"acceptedMaxResponseBytes");let f=l;if(!e?.responseBudgetVerified){let _=await ri(n,r,e?.authorization,l,e?.fetch??globalThis.fetch);if(!_.acceptMaxResponseBytesSupport)throw new k("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true before RPC dispatch","");f=fr(l,_.maxResponseBytes??void 0)??l}a[Or]=String(l);let p=e?.fetch??globalThis.fetch;return async(_,g)=>{let w=wT(_,g),I=o!=null&&s?await s(w,o):w,x=await p(n+Zg(pa,_,{prefix:r}),{method:"POST",headers:a,body:I});if(x.status===401)throw new k("AuthenticationError","Authentication required","");if(x.status===404&&!(x.headers.get("Content-Type")??"").startsWith(Cr)){let nt=await x.text().catch(()=>"");throw new k("HttpError",`HTTP 404: ${nt.slice(0,200)}`,"")}let C=Li(x.headers);f=fr(f,C.maxResponseBytes??void 0)??f;let D=await Pi(x,f),Y=new Uint8Array(await uo(x.headers,D,c,f)),{batches:M}=await Fr(Y);return bT(M,void 0,e?.externalLocation)}}async function zp(t,e){let n=await qp(t,e),r,a=e?.protocol;if(!a)r=xb(await n(mb)),a=gT(r);return _T(Sb(await n(Au,a)),r)}function Eb(t,e){return Object.defineProperty(t,xT,{value:e,enumerable:!1,configurable:!1}),t}var xT;var Db=b(()=>{Ke();Er();hb();Br();Cs();Rs();da();lo();Ab();Ns();Fs();Us();xT=Symbol.for("@query-farm/vgi-rpc/reflection-call")});function jp(t){if(bs.name==="arrow-js")return"active";let e=t;return e!=null&&typeof e.data==="object"&&e.data!==null&&!Array.isArray(e.children)?"arrow-js":"active"}function ST(t,e){if(jp(t)===e)return t;return e==="active"?vr(ei(t)):vr(ei(t))}function Tu(t,e){let n=t;return jp(n)==="arrow-js"?ur(n,e):ur(n,e)}function Ms(t,e){let n=e;if(n.length>0&&n.every((r)=>jp(r)==="arrow-js"))return As(n[0].schema,n);return Rr(t,n.map((r)=>ST(r,"active")))}var vu=b(()=>{Er();Er();da()});function Vi(t){return{batch:t,metadata:new Map(t.metadata??[])}}function Ls(t,e){let n=new Map(t.metadata);if(e)for(let[r,a]of e)n.set(r,a);return Tu(t.batch,n)}var $p=b(()=>{vu()});function AT(t,e){return e===null?t:`${t.length}:${t}${e}`}function Hp(t){let e=t.indexOf(":");if(e<0)return{cursor:t,callToken:null};let n=Number(t.slice(0,e));if(!Number.isInteger(n)||n<0)return{cursor:t,callToken:null};let r=t.slice(e+1);if(n>r.length)return{cursor:t,callToken:null};let a=r.slice(n);return{cursor:r.slice(0,n),callToken:a===""?null:a}}var Ps;var Bb=b(()=>{Er();Br();Cs();Rs();da();lo();Ns();Fs();Us();vu();$p();Ps=class Ps{_baseUrl;_prefix;_method;_stateToken;_callStateToken;_outputSchema;_inputSchema;_onLog;_pendingBatches;_finished;_header;_rawHeader;_compressionLevel;_compressFn;_decompressFn;_authorization;_externalConfig;_postFn;_acceptedMaxResponseBytes;_responseBudgetSupport=null;constructor(t){if(this._baseUrl=t.baseUrl,this._prefix=t.prefix,this._method=t.method,this._stateToken=t.stateToken,this._callStateToken=t.callStateToken??null,this._outputSchema=t.outputSchema,this._inputSchema=t.inputSchema,this._onLog=t.onLog,this._pendingBatches=t.pendingBatches,this._finished=t.finished,this._header=t.header,this._rawHeader=t.rawHeader??null,this._compressionLevel=t.compressionLevel,this._compressFn=t.compressFn,this._decompressFn=t.decompressFn,this._authorization=t.authorization,this._externalConfig=t.externalConfig,this._postFn=t.postFn,this._acceptedMaxResponseBytes=t.acceptedMaxResponseBytes??Dr,Nr(this._acceptedMaxResponseBytes,"acceptedMaxResponseBytes"),t.pendingBatches.filter((n)=>n.numRows>0||Ne(n)).length>1)throw new k("ProtocolError","A stream init returned more than one data batch","")}async _post(t,e){if(this._postFn)return this._postFn(t,e);if(!this._responseBudgetSupport)this._responseBudgetSupport=ri(this._baseUrl,this._prefix,this._authorization,this._acceptedMaxResponseBytes).then((a)=>{if(!a.acceptMaxResponseBytesSupport)throw new k("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true before RPC dispatch","");this._acceptedMaxResponseBytes=fr(this._acceptedMaxResponseBytes,a.maxResponseBytes??void 0)??this._acceptedMaxResponseBytes});await this._responseBudgetSupport;let n=await fetch(t,{method:"POST",headers:this._buildHeaders(),body:await this._prepareBody(e)}),r=Li(n.headers);return this._acceptedMaxResponseBytes=fr(this._acceptedMaxResponseBytes,r.maxResponseBytes??void 0)??this._acceptedMaxResponseBytes,n}get header(){return this._header}get rawHeader(){return this._rawHeader}_tokenMetadata(t,e){let n=new Map;if(n.set(ni,t),e?.cancel)n.set(vg,"1");if(this._callStateToken!==null)n.set(vs,this._callStateToken);return n}_resumeToken(){return this._stateToken===null?null:AT(this._stateToken,this._callStateToken)}_buildHeaders(){let t={"Content-Type":Cr};if(this._compressionLevel!=null&&this._compressFn)t["Content-Encoding"]="zstd";if(this._compressionLevel!=null&&this._decompressFn)t["Accept-Encoding"]="zstd";if(t[so]=co(this._decompressFn!=null),t[Or]=String(this._acceptedMaxResponseBytes),this._authorization)t.Authorization=this._authorization;return t}async _prepareBody(t){if(this._compressionLevel!=null&&this._compressFn)return await this._compressFn(t,this._compressionLevel);return t}async _readResponse(t){let e=await Pi(t,this._acceptedMaxResponseBytes),n=new Uint8Array(await uo(t.headers,e,this._decompressFn,this._acceptedMaxResponseBytes));if(n.byteLength>this._acceptedMaxResponseBytes)throw new k("TransportError",`Decoded HTTP response exceeds accepted limit (${n.byteLength} > ${this._acceptedMaxResponseBytes})`,"");return n}async exchange(t){if(this._stateToken===null)throw new k("ProtocolError","Stream has finished — no state token available","");if(!Array.isArray(t)){let a=new Map(t.metadata??[]);for(let[s,c]of this._tokenMetadata(this._stateToken))a.set(s,c);let o=Tu(t,a);return this._rowsOfExchange(await this._doExchange(o.schema,[o]))}if(t.length===0){let a=this._inputSchema??this._outputSchema,o=Ui(a,this._tokenMetadata(this._stateToken));return this._rowsOfExchange(await this._doExchange(a,[o]))}let e=this._inputSchema;if(!e){let o=Object.keys(t[0]).map((s)=>{let c;for(let p of t)if(p[s]!=null){c=p[s];break}let l=Tb(c),f=t.some((p)=>p[s]==null);return bt(s,l,f)});e=Oe(o)}let n={};for(let a of e.fields)n[a.name]=t.map((o)=>o[a.name]);let r=ur(lu(e,n),this._tokenMetadata(this._stateToken));return this._rowsOfExchange(await this._doExchange(e,[r]))}async tick(t){let e=await this.tickRaw(t);return e===null?[]:ii(e.batch)}async tickRaw(t){let e=await this.nextWithTokenRaw(t);return e===null?null:e.item}async exchangeRaw(t){if(this._stateToken===null)throw new k("ProtocolError","Stream has finished — no state token available","");let e=Ls(t,this._tokenMetadata(this._stateToken)),n=await this._doExchange(e.schema,[e]);return n===null?null:Vi(n)}async cancel(){if(this._finished||this._stateToken===null){this._finished=!0,this._stateToken=null;return}let t=this._stateToken;this._finished=!0,this._stateToken=null;let e=Oe([]),n=Ui(e,this._tokenMetadata(t,{cancel:!0}));try{let r=await this._post(this._baseUrl+Mi(this._prefix,this._method,{suffix:"/exchange"}),Rr(e,[n]));await this._readResponse(r)}catch{}}async _doExchange(t,e){let n=Ms(t,e),r=await this._post(this._baseUrl+Mi(this._prefix,this._method,{suffix:"/exchange"}),n);if(r.status===401)throw new k("AuthenticationError","Authentication required","");let a=await this._readResponse(r),{batches:o}=await Fr(a),s=null;for(let c of o){let l=Ne(c);if(c.numRows===0&&!l&&me(c,this._onLog))continue;let f=l?await Hn(c,this._externalConfig,this._onLog):c,p=f.metadata?.get(ni)??c.metadata?.get(ni);if(s!==null){if(p&&c.numRows===0&&!l){this._stateToken=p;continue}throw new k("ProtocolError","An exchange turn returned more than one data batch","")}if(p)this._stateToken=p;s=f}return s}_rowsOfExchange(t){return t===null?[]:ii(t)}async*[Symbol.asyncIterator](){for(let t of this._pendingBatches){if(t.numRows===0)if(Ne(t))t=await Hn(t,this._externalConfig,this._onLog);else{me(t,this._onLog);continue}yield ii(t)}if(this._pendingBatches=[],this._finished)return;if(this._stateToken===null)return;while(!0){let t=this._stateToken;if(t===null)return;let e=await this._sendContinuation(t),{batches:n}=await Fr(e),r=!1,a=null;for(let o of n){if(o.numRows===0){let s=o.metadata?.get(ni);if(s){this._stateToken=s,r=!0;continue}if(Ne(o))o=await Hn(o,this._externalConfig,this._onLog);else{me(o,this._onLog);continue}}if(a!==null)throw new k("ProtocolError","A producer turn returned more than one data batch","");a=ii(o)}if(a!==null)yield a;if(!r)break}}async nextWithToken(){let t=await this.nextWithTokenRaw();return t===null?null:{rows:ii(t.item.batch),token:t.token}}async nextWithTokenRaw(t){if(t!==void 0&&this._pendingBatches.length>0)throw new k("ProtocolError","Consume the producer's init batch before sending an explicit tick","");while(this._pendingBatches.length>0){let s=this._pendingBatches.shift();if(s.numRows===0)if(Ne(s))s=await Hn(s,this._externalConfig,this._onLog);else{me(s,this._onLog);continue}if(this._pendingBatches.some((c)=>c.numRows>0||Ne(c)))throw new k("ProtocolError","A producer turn returned more than one data batch","");return{item:Vi(s),token:this._resumeToken()}}if(this._finished||this._stateToken===null)return this._finished=!0,null;let n=await this._sendContinuation(this._stateToken,t),{batches:r}=await Fr(n),a=null,o=null;for(let s of r){if(s.numRows===0){let c=s.metadata?.get(ni);if(c){o=c;continue}if(Ne(s))s=await Hn(s,this._externalConfig,this._onLog);else{me(s,this._onLog);continue}}if(a!==null)throw new k("ProtocolError","A producer turn returned more than one data batch","");a=s}if(this._stateToken=o,a===null)return this._finished=!0,null;return{item:Vi(a),token:this._resumeToken()}}seekToToken(t){let{cursor:e,callToken:n}=Hp(t);this._pendingBatches=[],this._stateToken=e,this._callStateToken=n,this._finished=!1}async _sendContinuation(t,e){let n=Oe([]),r=new Map(e??[]);for(let[c,l]of this._tokenMetadata(t))r.set(c,l);let a=io(n,{},r),o=Rr(n,[a]),s=await this._post(this._baseUrl+Mi(this._prefix,this._method,{suffix:"/exchange"}),o);if(s.status===401)throw new k("AuthenticationError","Authentication required","");return this._readResponse(s)}close(){}}});async function Yp(t,e,n,r,a=globalThis.fetch,o=Dr,s=!1){Nr(o,"acceptedMaxResponseBytes");let c=o;if(!s){let x=await ri(t,e,r,o,a);if(!x.acceptMaxResponseBytesSupport)throw new k("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true before RPC dispatch","");c=fr(o,x.maxResponseBytes??void 0)??o}let l=ki(tb,{count:BigInt(n)},Up),f={"Content-Type":Cr};if(r)f.Authorization=r;f[Or]=String(o);let p=await a(t+bu(`${Up}/init`,{prefix:e}),{method:"POST",headers:f,body:l});if(p.status===404)throw new k("NotSupported","Server does not support upload URLs","");if(p.status===401)throw new k("AuthenticationError","Authentication required","");if(!p.ok)throw new k("HttpError",`__upload_url__/init failed: HTTP ${p.status}`,"");let _=Li(p.headers);c=fr(c,_.maxResponseBytes??void 0)??c;let g=await Pi(p,c),w=await Ot.from(g);await w.open();let I=[];for(let x of w.readAll()){if(x.numRows===0)continue;for(let C=0;C{Ke();Er();Br();Cs();da();lo();Ns();Fs();Us()});function TT(t,e){let n=t.replace(/\/+$/,""),r=(e?.prefix??"").replace(/\/+$/,""),a=e?.onLog,o=e?.compressionLevel,s=e?.authorization,c=e?.externalLocation,l=e?.fetch??globalThis.fetch,f=e?.acceptedMaxResponseBytes??Dr;Nr(f,"acceptedMaxResponseBytes");let p=[];function _(){return p.length===0?null:p[p.length-1]}function g(T,F){let wt=new Headers(F?.headers);if(wt.set(eb,"true"),T.token!==null)wt.set(wu,T.token);for(let[ot,At]of Object.entries(T.echo))if(!wt.has(ot))wt.set(ot,At);return wt}function w(T,F){let wt=F.headers.get(wu);if(wt)T.token=wt;let ot=Mp.toLowerCase();if(F.headers.forEach((At,Ut)=>{if(Ut.toLowerCase().startsWith(ot))T.echo[Ut.slice(Mp.length)]=At}),(F.headers.get(nb)??"").trim().toLowerCase()==="true")T.token=null,T.echo={}}let I=async(T,F)=>{let wt=_();if(wt===null)return l(T,F);if(!(typeof T==="string"?T:T instanceof URL?T.href:T.url).startsWith(n)||(F?.method??"GET").toUpperCase()==="PUT")return l(T,F);let At=await l(T,{...F,headers:g(wt,F)});return w(wt,At),At},x=c?{...c,fetch:I}:c,C=e?.description?new Map(e.description.methods.map((T)=>[T.name,T])):null,D=e?.description?.protocolVersion??"",Y=e?.description?.protocolName??"";function M(){if(!Y)throw new k("ProtocolError","The server did not report a protocol name, so no RPC path can be built. Every request must name the protocol it addresses.","");return`${r}/${Y}`}let nt,lt,Z=!1,Ct=null,xe=null;function sn(T){let F=Li(T.headers);if(F.maxRequestBytes!=null||F.maxResponseBytes!=null||F.uploadUrlSupport||F.acceptMaxResponseBytesSupport)Ct=Ct?{...F,maxRequestBytes:F.maxRequestBytes??Ct.maxRequestBytes,maxResponseBytes:F.maxResponseBytes??Ct.maxResponseBytes,maxUploadBytes:F.maxUploadBytes??Ct.maxUploadBytes}:F}async function Zt(){if(!xe)xe=ri(n,r,s,f,I).then((T)=>{if(!T.acceptMaxResponseBytesSupport)throw new k("ProtocolError","Server must advertise VGI-Accept-Max-Response-Bytes-Support: true before RPC dispatch","");Ct=T}).catch((T)=>{throw xe=null,T});await xe}function ye(){return fr(f,Ct?.maxResponseBytes??void 0)??f}async function vn(T){let F=fb(Ct)?Ct:null;if(!F)return T;if(!F.uploadUrlSupport)return T;if(F.maxRequestBytes==null||T.byteLength<=F.maxRequestBytes)return T;return Kp(T,{baseUrl:n,prefix:r,authorization:s,urlValidator:c?.urlValidator??null,fetch:I,acceptedMaxResponseBytes:f,responseBudgetVerified:!0})}async function _e(T,F){await Zt();let wt=await vn(F),ot=await I(T,{method:"POST",headers:dt(),body:await Se(wt)});if(sn(ot),ot.status===413&&Ct?.uploadUrlSupport&&F.byteLength>0){let At=await Kp(F,{baseUrl:n,prefix:r,authorization:s,urlValidator:c?.urlValidator??null,fetch:I,acceptedMaxResponseBytes:f,responseBudgetVerified:!0});ot=await I(T,{method:"POST",headers:dt(),body:await Se(At)}),sn(ot)}return ot}async function mt(){if(Z||o==null)return;try{let T=await Promise.resolve().then(() => (Fp(),{}));nt=Hg,lt=Np}catch{}Z=!0}function dt(){let T={"Content-Type":Cr};if(o!=null&&nt)T["Content-Encoding"]="zstd";if(o!=null&<)T["Accept-Encoding"]="zstd";if(T[so]=co(lt!=null),T[Or]=String(f),s)T.Authorization=s;return T}async function Se(T){if(o!=null&&nt)return await nt(T,o);return T}function ai(T){if(T.status===401)throw new k("AuthenticationError","Authentication required","")}async function oi(T){let F=ye(),wt=await Pi(T,F),ot=new Uint8Array(await uo(T.headers,wt,lt,F));if(ot.byteLength>F)throw new k("TransportError",`Decoded HTTP response exceeds accepted limit (${ot.byteLength} > ${F})`,"");return ot}async function Ur(){if(C)return C;await Zt(),await mt();let T=await zp(n,{prefix:r,protocol:e?.protocol,externalLocation:x,authorization:s,compressionLevel:o,compressFn:nt,decompressFn:lt,acceptedMaxResponseBytes:ye(),fetch:I,responseBudgetVerified:!0});return C=new Map(T.methods.map((F)=>[F.name,F])),D=T.protocolVersion,Y=T.protocolName,C}async function ha(T){let F=e?.protocol??T.get(uu);if(F)return`${r}/${F}`;return await Ur(),M()}async function po(T,F,wt,ot){let At=await _e(n+Mi(ot??M(),T,{suffix:"/init"}),F);ai(At);let Ut=await oi(At),Mt=null,Ht=null,cn=null,te=[],qi=0,ma=(ya)=>{if(qi+=1,qi>1)throw new k("ProtocolError","A stream init returned more than one data batch","");te.push(ya)},pr=!1,ho=null;if(wt){let ya=await vb(Ut),zs=await ya.readStream();if(zs)for(let ee of zs.batches){if(ee.numRows===0){if(Ne(ee)){Mt=await Hn(ee,x,a);continue}me(ee,a);continue}Mt=ee}let hr=await ya.readStream();if(hr)ho=hr.schema;let Fe=[];if(hr)for(let ee of hr.batches){if(ee.numRows===0){let Ru=ee.metadata?.get(ni);if(Ru){Ht=Ru,cn=ee.metadata?.get(vs)??cn;continue}if(Ne(ee)){ma(ee);continue}if(ee.metadata?.get(la)==="EXCEPTION"){Fe.push(ee);continue}me(ee,a);continue}ma(ee)}if(Fe.length>0)if(te.length>0||Ht!==null)te.push(...Fe);else for(let ee of Fe)me(ee,a);if(!hr&&!Ht)pr=!0}else{let{schema:ya,batches:zs}=await Fr(Ut);ho=ya;let hr=[];for(let Fe of zs){if(Fe.numRows===0){let ee=Fe.metadata?.get(ni);if(ee){Ht=ee,cn=Fe.metadata?.get(vs)??cn;continue}if(Ne(Fe)){ma(Fe);continue}if(Fe.metadata?.get(la)==="EXCEPTION"){hr.push(Fe);continue}me(Fe,a);continue}ma(Fe)}if(hr.length>0)if(te.length>0||Ht!==null)te.push(...hr);else for(let Fe of hr)me(Fe,a)}if(te.length===0&&Ht===null)pr=!0;let Hb=(ho&&ho.fields.length>0?ho:null)??(te.length>0?te[0].schema:null);return{headerBatch:Mt,stateToken:Ht,callStateToken:cn,pendingBatches:te,finished:pr,outputSchema:Hb}}return Eb({async call(T,F){await mt();let ot=(await Ur()).get(T);if(!ot)throw Error(`Unknown method: '${T}'`);let At={...ot.defaults??{},...F??{}},Ut=ki(ot.paramsSchema,At,T,{protocolVersion:D,protocol:Y}),Mt=await _e(n+Mi(M(),T),Ut);ai(Mt);let Ht=await oi(Mt),{batches:cn}=await Fr(Ht),te=null;for(let pr of cn){if(pr.numRows===0)if(Ne(pr))pr=await Hn(pr,x,a);else{me(pr,a);continue}if(te!==null)throw new k("ProtocolError","A unary response returned more than one data batch","");te=pr}if(!te)return null;let qi=ii(te);if(qi.length===0)return null;let ma=qi[0];if(ot.resultSchema.fields.length===0)return null;return ma},async stream(T,F){await mt();let ot=(await Ur()).get(T);if(!ot)throw Error(`Unknown method: '${T}'`);let At={...ot.defaults??{},...F??{}},Ut=ki(ot.paramsSchema,At,T,{protocolVersion:D,protocol:Y}),Mt=await po(T,Ut,ot.headerSchema!=null),Ht=Mt.headerBatch===null?null:ii(Mt.headerBatch)[0]??null;return new Ps({baseUrl:n,prefix:M(),method:T,stateToken:Mt.stateToken,callStateToken:Mt.callStateToken,outputSchema:Mt.outputSchema??ot.outputSchema??ot.resultSchema,inputSchema:ot.inputSchema,onLog:a,pendingBatches:Mt.pendingBatches,finished:Mt.finished,header:Ht,rawHeader:Mt.headerBatch===null?null:Vi(Mt.headerBatch),compressionLevel:o,compressFn:nt,decompressFn:lt,authorization:s,externalConfig:x,acceptedMaxResponseBytes:ye(),postFn:_e})},async callRaw(T,F){await mt();let wt=Ls(F),ot=await _e(n+Mi(await ha(F.metadata),T),Ms(wt.schema,[wt]));ai(ot);let At=await oi(ot),{batches:Ut}=await Fr(At),Mt=null;for(let Ht of Ut){if(Ht.numRows===0)if(Ne(Ht))Ht=await Hn(Ht,x,a);else{me(Ht,a);continue}if(Mt!==null)throw new k("ProtocolError","A unary response returned more than one data batch","");Mt=Ht}return Mt===null?null:Vi(Mt)},async streamRaw(T,F,wt){await mt();let ot=Ls(F),At=await ha(F.metadata),Ut=await po(T,Ms(ot.schema,[ot]),wt.hasHeader,At);return new Ps({baseUrl:n,prefix:At,method:T,stateToken:Ut.stateToken,callStateToken:Ut.callStateToken,outputSchema:Ut.outputSchema??Oe([]),onLog:a,pendingBatches:Ut.pendingBatches,finished:Ut.finished,header:null,rawHeader:Ut.headerBatch===null?null:Vi(Ut.headerBatch),compressionLevel:o,compressFn:nt,decompressFn:lt,authorization:s,externalConfig:x,acceptedMaxResponseBytes:ye(),postFn:_e})},async resumeStream(T,F,wt){await mt(),await Zt(),await Ur();let{cursor:ot,callToken:At}=Hp(F);return new Ps({baseUrl:n,prefix:M(),method:T,stateToken:ot,callStateToken:At,outputSchema:wt??Oe([]),onLog:a,pendingBatches:[],finished:!1,header:null,compressionLevel:o,compressFn:nt,decompressFn:lt,authorization:s,externalConfig:x,acceptedMaxResponseBytes:ye(),postFn:_e})},async describe(){return await mt(),await Zt(),zp(n,{prefix:r,protocol:e?.protocol,externalLocation:x,authorization:s,compressionLevel:o,compressFn:nt,decompressFn:lt,acceptedMaxResponseBytes:ye(),fetch:I,responseBudgetVerified:!0})},async capabilities(){let T=await ri(n,r,s,f,I);return Ct=T,T},async requestUploadUrls(T=1){return Yp(n,r,T,s,I,f)},beginSession(T){p.push({token:T?T:null,echo:{}})},currentSessionToken(){return _()?.token??null},currentEchoHeaders(){return{..._()?.echo??{}}},detachSession(){let T=_();if(T===null)return null;let F=T.token;return T.token=null,T.echo={},F},async endSession(){let T=p.pop();if(T===void 0)return;let{token:F,echo:wt}=T;if(F===null)return;let ot={[wu]:F};if(s)ot.Authorization=s;for(let[At,Ut]of Object.entries(wt))ot[At]=Ut;try{await l(n+bu(rb,{prefix:r}),{method:"DELETE",headers:ot})}catch{}},close(){}},async(T,F)=>(await mt(),await Zt(),(await qp(n,{prefix:r,externalLocation:x,authorization:s,compressionLevel:o,compressFn:nt,decompressFn:lt,acceptedMaxResponseBytes:ye(),fetch:I,responseBudgetVerified:!0}))(T,F)))}var Gp=b(()=>{Er();Br();Cs();Rs();da();lo();Ns();Fs();Db();Us();vu();$p();Bb();Cb();Br()});var kb=()=>{};var Vb=b(()=>{kb()});var qb=b(()=>{Vb()});var X={Null:1,Int:2,Float:3,Binary:4,Utf8:5,Bool:6,Decimal:7,Date:8,Time:9,Timestamp:10,Interval:11,List:12,Struct:13,Union:14,FixedSizeBinary:15,FixedSizeList:16,Map:17,Duration:18,LargeBinary:19,LargeUtf8:20,Dictionary:-1};function Mr(t){return t.typeId===X.Int}function Ou(t){return t.typeId===X.Float}function mo(t){return t.typeId===X.Binary||t.typeId===X.LargeBinary}function Nu(t){return t.typeId===X.Utf8||t.typeId===X.LargeUtf8}function yo(t){return t.typeId===X.Bool}function yr(t){return t.typeId===X.List}function Lr(t){return t.typeId===X.Struct}function _r(t){return t.typeId===X.Map}function _o(t){return t.typeId===X.Dictionary}function si(t){if(t==null)return"null";let e=t,n=()=>Array.isArray(e.children)?e.children.map((r)=>`${r?.name??""}:${si(r?.type??r)}`).join(","):"";switch(t.typeId){case X.Int:return`int${e.bitWidth}${e.isSigned??e.signed??!0?"":"u"}`;case X.Float:return`float${e.precision}`;case X.Decimal:return`decimal${e.bitWidth??128}(${e.precision},${e.scale})`;case X.Date:case X.Time:case X.Duration:return`${t.typeId}[${e.unit}]`;case X.Timestamp:return`ts[${e.unit},${e.timezone??""}]`;case X.Interval:return`iv[${e.unit}]`;case X.FixedSizeBinary:return`fsb(${e.byteWidth??e.stride})`;case X.FixedSizeList:return`fsl(${e.listSize??e.stride},${n()})`;case X.List:case X.Struct:case X.Map:return`${t.typeId}<${n()}>`;case X.Union:return`union(${e.mode},${n()})`;case X.Dictionary:return`dict(${si(e.dictionary)},${si(e.indices)},${e.ordered??e.isOrdered??!1})`;default:return Array.isArray(e.children)?`${t.typeId}<${n()}>`:String(t.typeId)}}var ap={};mr(ap,{DateUnit:()=>R_,IntervalUnit:()=>O_,TimeUnit:()=>aa,UnionMode:()=>N_,adoptArrowJsShape:()=>ip,backend:()=>z_,batchFromColumns:()=>ue,batchFromRows:()=>ke,batchToScalarDict:()=>Ri,batchToSecretDict:()=>zl,binary:()=>Nl,bool:()=>vl,buildStatisticsBatch:()=>rp,columnFromArray:()=>ep,date:()=>as,dateDay:()=>jd,dateMillisecond:()=>$d,decimal:()=>is,decimal128:()=>qd,decimal256:()=>zd,decodeDictValue:()=>ql,decodeMapValue:()=>jl,denseUnion:()=>Zd,deserializeBatch:()=>fe,deserializeBatches:()=>ps,deserializeSchema:()=>We,dictionary:()=>Pl,duration:()=>Wd,emptyBatch:()=>An,field:()=>Tr,filterBatch:()=>fs,fixedSizeBinary:()=>Vd,fixedSizeList:()=>Xd,float16:()=>Ld,float32:()=>Pd,float64:()=>Rl,int:()=>Md,int16:()=>Ga,int32:()=>es,int64:()=>ns,int8:()=>El,interval:()=>Qd,iterRows:()=>on,largeBinary:()=>Fl,largeUtf8:()=>kd,list:()=>Ml,map:()=>Ll,nullType:()=>Tl,projectBatch:()=>ds,projectSchema:()=>Xa,readCanonicalValue:()=>Ce,safeNumber:()=>us,schema:()=>F_,serializeBatch:()=>Yt,serializeBatches:()=>$l,serializeCollectionRows:()=>Hl,serializeSchema:()=>cr,sparseUnion:()=>Jd,struct:()=>os,time:()=>oa,timeMicrosecond:()=>Kd,timeMillisecond:()=>Yd,timeNanosecond:()=>Gd,timeSecond:()=>Hd,timestamp:()=>Ul,uint16:()=>Bl,uint32:()=>Cl,uint64:()=>rs,uint8:()=>Dl,union:()=>ss,utf8:()=>Ol,withBatchMetadata:()=>np});Ke();var aa={SECOND:E.SECOND,MILLISECOND:E.MILLISECOND,MICROSECOND:E.MICROSECOND,NANOSECOND:E.NANOSECOND},R_={DAY:vt.DAY,MILLISECOND:vt.MILLISECOND},O_={YEAR_MONTH:It.YEAR_MONTH,DAY_TIME:It.DAY_TIME,MONTH_DAY_NANO:It.MONTH_DAY_NANO},N_={Sparse:yt.Sparse,Dense:yt.Dense};function F_(t,e){return new P(t,e??new Map)}function Tr(t,e,n=!0,r){return new N(t,e,n,r??new Map)}var Tl=()=>new zt,vl=()=>new jt,El=()=>new kr,Ga=()=>new Vr,es=()=>new Ee,ns=()=>new De,Dl=()=>new qr,Bl=()=>new zr,Cl=()=>new jr,rs=()=>new qn;function Md(t=32,e=!0){if(e)switch(t){case 8:return El();case 16:return Ga();case 32:return es();case 64:return ns()}else switch(t){case 8:return Dl();case 16:return Bl();case 32:return Cl();case 64:return rs()}throw Error(`int: unsupported bitWidth ${t}`)}var Ld=()=>new Ia,Pd=()=>new $r,Rl=()=>new _n,Ol=()=>new Gt,kd=()=>new Pn,Nl=()=>new ze,Fl=()=>new Ln,Vd=(t)=>new Vn(t);function is(t,e,n=128){return new kn(e,t,n)}var qd=(t,e)=>is(t,e,128),zd=(t,e)=>is(t,e,256),as=(t=vt.MILLISECOND)=>new tr(t),jd=()=>as(vt.DAY),$d=()=>as(vt.MILLISECOND);function oa(t=E.MILLISECOND,e){let n=e??(t<=E.MILLISECOND?32:64);return new er(t,n)}var Hd=()=>oa(E.SECOND,32),Yd=()=>oa(E.MILLISECOND,32),Kd=()=>oa(E.MICROSECOND,64),Gd=()=>oa(E.NANOSECOND,64),Ul=(t=E.MILLISECOND,e=null)=>new en(t,e),Wd=(t=E.MILLISECOND)=>new rr(t),Qd=(t=It.MONTH_DAY_NANO)=>new nr(t);function Ml(t){let e=ts(t)?t:Tr("item",t,!0);return new ve(e)}function Xd(t,e){let n=ts(t)?t:Tr("item",t,!0);return new yn(e,n)}function os(t){return new it(t)}function Ll(t,e,n=!1){let r=ts(t)?t:Tr("key",t,!1),a=ts(e)?e:Tr("value",e,!0),o=Tr("entries",os([r,a]),!1);return new nn(o,n)}function Pl(t,e=Ga(),n=!1,r){return new be(t,e,r,n)}function ss(t,e,n){let r=e.map((s,c)=>ts(s)?s:Tr(String(c),s,!0)),a=n??r.map((s,c)=>c);return new(t===yt.Dense?Ro:bi)(a,r)}var Jd=(t,e)=>ss(yt.Sparse,t,e),Zd=(t,e)=>ss(yt.Dense,t,e);function ts(t){return typeof t.name==="string"&&t.type!=null}Ke();function kl(t){let e=t.type;if(S.isList(e)){let n=e.children[0];return A({type:e,length:0,nullCount:0,child:kl(n),valueOffsets:new Int32Array([0])})}if(S.isMap(e)){let n=e.children[0];return A({type:e,length:0,nullCount:0,child:kl(n),valueOffsets:new Int32Array([0])})}if(S.isStruct(e)){let n=e.children.map((r)=>kl(r));return A({type:e,length:0,nullCount:0,children:n})}return A({type:e,length:0,nullCount:0})}function An(t){let e=t,n=e.fields.map((o)=>kl(o)),r=new it(e.fields),a=A({type:r,length:0,children:n,nullCount:0});return new rt(e,a)}Ke();var cs=86400000;function Et(t){return t===null||t===void 0}function ft(t,e,n){let r;try{r=typeof n==="bigint"?`${n}n`:typeof n==="object"?Object.prototype.toString.call(n):String(n)}catch{r=""}throw TypeError(`codec[${t}]: ${e} (got ${typeof n}: ${r})`)}function In(t,e){let n=(r)=>{if(Et(r))return null;return e(r),r};return{label:t,richToCanonical:n,canonicalToRich:n,rawToCanonical:n,canonicalToRaw:n}}function kS(t){return(e)=>{if(typeof e!=="boolean")ft(t,"expected boolean",e)}}function Wa(t,e,n){return(r)=>{let a;if(typeof r==="number")a=r;else if(typeof r==="bigint"){if(rBigInt(n))ft(t,`bigint out of ${t} range`,r);return}else return ft(t,"expected number",r);if(!Number.isInteger(a))ft(t,"expected an integer",r);if(an)ft(t,`value out of ${t} range [${e}, ${n}]`,r)}}function VS(t){return(e)=>{if(typeof e==="number")return;if(typeof e==="bigint")return;ft(t,"expected number",e)}}function M_(t,e,n){let r=n?(1n<{let s;if(typeof o==="bigint")s=o;else if(typeof o==="number"){if(!Number.isInteger(o))ft(t,"expected an integer",o);s=BigInt(o)}else return ft(t,"expected bigint",o);if(sr)ft(t,`value out of ${t} range`,o)}}function U_(t){return(e)=>{if(typeof e!=="string")ft(t,"expected string",e)}}function ls(t,e){if(typeof e==="bigint")return e;if(typeof e==="number"){if(!Number.isInteger(e))ft(t,"expected an integer value",e);return BigInt(e)}return ft(t,"expected number or bigint",e)}function sa(t,e){if(e<-9007199254740991n||e>9007199254740991n)ft(t,"bigint overflows the safe-integer range when narrowing to number",e);return Number(e)}function qS(){return{label:"date32",richToCanonical(e){if(Et(e))return null;if(e instanceof Date){let n=e.getTime();if(Number.isNaN(n))ft("date32","invalid Date",e);if(n%cs!==0)return Math.floor(n/cs);return n/cs}if(typeof e==="number"){if(!Number.isInteger(e))ft("date32","day-number must be an integer",e);return e}if(typeof e==="bigint")return sa("date32",e);return ft("date32","expected Date, number (days), or bigint",e)},canonicalToRich(e){if(Et(e))return null;let n;if(typeof e==="number"){if(!Number.isInteger(e))ft("date32","canonical day-number must be an integer",e);n=e}else if(typeof e==="bigint")n=sa("date32",e);else if(e instanceof Date)return e;else return ft("date32","expected number (days) canonical",e);let r=n*cs,a=new Date(r);if(Number.isNaN(a.getTime()))ft("date32","day-number produces an invalid Date",e);return a},rawToCanonical(e){if(Et(e))return null;if(typeof e==="number"){if(!Number.isInteger(e))ft("date32","raw day-number must be an integer",e);return e}if(typeof e==="bigint")return sa("date32",e);return ft("date32","expected branded Date32 (day-number)",e)},canonicalToRaw(e){if(Et(e))return null;if(typeof e==="number"){if(!Number.isInteger(e))ft("date32","canonical day-number must be an integer",e);return e}if(typeof e==="bigint")return sa("date32",e);if(e instanceof Date){let n=e.getTime();if(Number.isNaN(n))ft("date32","invalid Date",e);return Math.floor(n/cs)}return ft("date32","expected number (days) canonical",e)}}}function zS(){return{label:"date64",richToCanonical(e){if(Et(e))return null;if(e instanceof Date){let n=e.getTime();if(Number.isNaN(n))ft("date64","invalid Date",e);return BigInt(n)}if(typeof e==="bigint")return e;if(typeof e==="number"){if(!Number.isInteger(e))ft("date64","ms value must be an integer",e);return BigInt(e)}return ft("date64","expected Date, bigint (ms), or number",e)},canonicalToRich(e){if(Et(e))return null;if(e instanceof Date)return e;let n=ls("date64",e),r=sa("date64",n),a=new Date(r);if(Number.isNaN(a.getTime()))ft("date64","ms value produces an invalid Date",e);return a},rawToCanonical(e){if(Et(e))return null;if(e instanceof Date)return ft("date64","raw Date64Ms must be a bigint (ms), not a Date",e);return ls("date64",e)},canonicalToRaw(e){if(Et(e))return null;if(e instanceof Date)return BigInt(e.getTime());return ls("date64",e)}}}function jS(){let e=(r)=>{if(Et(r))return null;if(typeof r==="number"){if(!Number.isInteger(r))ft("time32","time32 value must be an integer",r);return r}if(typeof r==="bigint")return sa("time32",r);return ft("time32","expected number (raw time unit)",r)},n=(r)=>{if(Et(r))return null;if(typeof r==="number")return r;if(typeof r==="bigint")return sa("time32",r);return ft("time32","expected number canonical",r)};return{label:"time32",richToCanonical:e,canonicalToRich:n,rawToCanonical:e,canonicalToRaw:n}}function tp(t){let e=(n)=>{if(Et(n))return null;return ls(t,n)};return{label:t,richToCanonical:e,canonicalToRich:e,rawToCanonical:e,canonicalToRaw:e}}function $S(t){let e=new Uint8Array(t.buffer,t.byteOffset,t.byteLength),n=0n;for(let r=e.length-1;r>=0;r--)n=n<<8n|BigInt(e[r]);if(e.length>0&&e[e.length-1]&128)n-=1n<{if(Et(r))return null;if(typeof r==="bigint")return r;if(typeof r==="number")return ls("decimal",r);if(ArrayBuffer.isView(r))return $S(r);return ft("decimal","expected bigint, number, or decimal byte view",r)},n=(r)=>{if(Et(r))return null;return e(r)};return{label:"decimal",richToCanonical:e,canonicalToRich:n,rawToCanonical:e,canonicalToRaw:n}}function YS(t){let n=t.children.map((a)=>({name:a.name,codec:Ge(a.type)})),r="struct";return{label:"struct",richToCanonical(a){if(Et(a))return null;if(typeof a!=="object")ft("struct","expected an object",a);let o=a,s={};for(let{name:c,codec:l}of n)s[c]=l.richToCanonical(o[c]??null);return s},canonicalToRich(a){if(Et(a))return null;if(typeof a!=="object")ft("struct","expected an object",a);let o=a,s={};for(let{name:c,codec:l}of n)s[c]=l.canonicalToRich(o[c]??null);return s},rawToCanonical(a){if(Et(a))return null;if(typeof a!=="object")ft("struct","expected an object",a);let o=a,s={};for(let{name:c,codec:l}of n)s[c]=l.rawToCanonical(o[c]??null);return s},canonicalToRaw(a){if(Et(a))return null;if(typeof a!=="object")ft("struct","expected an object",a);let o=a,s={};for(let{name:c,codec:l}of n)s[c]=l.canonicalToRaw(o[c]??null);return s}}}function KS(t){let e=t.children?.[0];if(!e)throw Error("codec[list]: list type has no child field");let n=Ge(e.type),r="list",a=(o)=>{if(Array.isArray(o))return o;if(o!=null&&typeof o[Symbol.iterator]==="function")return Array.from(o);return ft(r,"expected an array or iterable",o)};return{label:r,richToCanonical(o){if(Et(o))return null;return a(o).map((s)=>n.richToCanonical(s))},canonicalToRich(o){if(Et(o))return null;return a(o).map((s)=>n.canonicalToRich(s))},rawToCanonical(o){if(Et(o))return null;return a(o).map((s)=>n.rawToCanonical(s))},canonicalToRaw(o){if(Et(o))return null;return a(o).map((s)=>n.canonicalToRaw(s))}}}function GS(t){let n=t.children?.[0]?.type?.children;if(!n||n.length<2)throw Error("codec[map]: map type has no key/value entry fields");let r=Ge(n[0].type),a=Ge(n[1].type),o="map",s=(c)=>{if(Array.isArray(c))return c;if(c instanceof Map)return Array.from(c.entries());if(c!=null&&typeof c[Symbol.iterator]==="function")return Array.from(c);if(c!=null&&typeof c==="object")return Object.entries(c);return ft(o,"expected Map, array of pairs, or object",c)};return{label:o,richToCanonical(c){if(Et(c))return null;return s(c).map(([l,f])=>[r.richToCanonical(l),a.richToCanonical(f)])},canonicalToRich(c){if(Et(c))return null;return s(c).map(([l,f])=>[r.canonicalToRich(l),a.canonicalToRich(f)])},rawToCanonical(c){if(Et(c))return null;return s(c).map(([l,f])=>[r.rawToCanonical(l),a.rawToCanonical(f)])},canonicalToRaw(c){if(Et(c))return null;return s(c).map(([l,f])=>[r.canonicalToRaw(l),a.canonicalToRaw(f)])}}}function WS(t){let e=t.dictionary;if(!e)throw Error("codec[dictionary]: missing dictionary value type");let n=Ge(e);return{label:`dictionary<${n.label}>`,richToCanonical:(r)=>n.richToCanonical(r),canonicalToRich:(r)=>n.canonicalToRich(r),rawToCanonical:(r)=>n.rawToCanonical(r),canonicalToRaw:(r)=>n.canonicalToRaw(r)}}var QS=In("int8",Wa("int8",-128,127)),XS=In("int16",Wa("int16",-32768,32767)),JS=In("int32",Wa("int32",-2147483648,2147483647)),ZS=In("uint8",Wa("uint8",0,255)),tA=In("uint16",Wa("uint16",0,65535)),eA=In("uint32",Wa("uint32",0,4294967295)),nA=In("int64",M_("int64",64,!0)),rA=In("uint64",M_("uint64",64,!1)),iA=In("float",VS("float")),aA=In("bool",kS("bool")),oA={label:"utf8",richToCanonical(t){if(Et(t))return null;return typeof t==="string"?t:String(t)},canonicalToRich(t){if(Et(t))return null;return U_("utf8")(t),t},rawToCanonical(t){if(Et(t))return null;return typeof t==="string"?t:String(t)},canonicalToRaw(t){if(Et(t))return null;return U_("utf8")(t),t}};function sA(t,e){return(n)=>{if(Et(n))return null;let r;if(n instanceof Uint8Array)r=n;else if(ArrayBuffer.isView(n)){let a=n;r=new Uint8Array(a.buffer,a.byteOffset,a.byteLength)}else return ft(t,"expected Uint8Array",n);if(e!=null&&r.length!==e)ft(t,`expected exactly ${e} bytes`,n);return r}}function L_(t,e){let n=sA(t,e);return{label:t,richToCanonical:n,canonicalToRich:n,rawToCanonical:n,canonicalToRaw:n}}var cA=L_("binary"),lA={label:"null",richToCanonical:()=>null,canonicalToRich:()=>null,rawToCanonical:()=>null,canonicalToRaw:()=>null};function Ge(t){switch(t.typeId){case X.Null:return lA;case X.Bool:return aA;case X.Int:{let e=t.bitWidth??32,n=t.isSigned??t.signed??!0;if(e===64)return n?nA:rA;if(n){if(e===8)return QS;if(e===16)return XS;return JS}if(e===8)return ZS;if(e===16)return tA;return eA}case X.Float:return iA;case X.Utf8:case X.LargeUtf8:return oA;case X.Binary:case X.LargeBinary:return cA;case X.FixedSizeBinary:return L_("fixedSizeBinary",t.byteWidth??t.stride);case X.Decimal:return HS();case X.Date:return t.unit===0?qS():zS();case X.Time:return(t.bitWidth??32)===64?tp("time64"):jS();case X.Timestamp:return tp("timestamp");case X.Duration:return tp("duration");case X.Struct:return YS(t);case X.Union:return In("union",()=>{});case X.List:case X.FixedSizeList:return KS(t);case X.Map:return GS(t);case X.Dictionary:return WS(t);case X.Interval:return In("interval",()=>{});default:return In(`type#${t.typeId}`,()=>{})}}Ke();function Vl(t,e){return Qa(e,t)}function Qa(t,e){if(S.isList(e))return mA(t,e);if(S.isFixedSizeList(e))return yA(t,e);if(S.isMap(e))return _A(t,e);if(S.isStruct(e))return gA(t,e);if(S.isDecimal(e))return dA(t,e);if(S.isDate(e)||S.isTime(e)||S.isTimestamp(e)||S.isDuration(e)){if(uA(e)===64)return P_(t,e);return fA(t,e)}if(S.isInt(e)&&e.bitWidth===64)return P_(t,e);return Ir(t,e).data[0]}function uA(t){if(S.isDate(t))return t.unit===vt.DAY?32:64;if(S.isTime(t))return t.bitWidth??32;return 64}function fA(t,e){let n=t.length,r=new Int32Array(n),a=new Uint8Array(Math.ceil(n/8)),o=0;for(let s=0;s>3]|=1<<(s&7),r[s]=typeof c==="bigint"?Number(c):c}return A({type:e,length:n,nullBitmap:o>0?a:void 0,data:r,nullCount:o})}function P_(t,e){let n=t.length,r=new BigInt64Array(n),a=new Uint8Array(Math.ceil(n/8)),o=0;for(let c=0;c>3]|=1<<(c&7),r[c]=typeof l==="bigint"?l:BigInt(l)}let s=new Int32Array(r.buffer);return A({type:e,length:n,nullBitmap:o>0?a:void 0,data:s,nullCount:o})}function dA(t,e){let n=e.byteWidth??(e.bitWidth?e.bitWidth/8:16),r=t.length,a=new Uint8Array(r*n),o=new Uint8Array(Math.ceil(r/8)),s=0;for(let c=0;c>3]|=1<<(c&7);let f;if(l instanceof Uint8Array)f=l;else if(ArrayBuffer.isView(l)){let p=l;f=new Uint8Array(p.buffer,p.byteOffset,p.byteLength)}else f=hA(pA(l),n);a.set(f.subarray(0,n),c*n)}return A({type:e,length:r,nullBitmap:s>0?o:void 0,data:new Uint32Array(a.buffer,a.byteOffset,a.byteLength/4),nullCount:s})}function pA(t){return typeof t==="bigint"?t:BigInt(Math.trunc(Number(t)))}function hA(t,e){let n=new Uint8Array(e),r=t;if(r<0n)r=(1n<>=8n;return n}function mA(t,e){let n=e.children[0],r=t.length,a=new Int32Array(r+1),o=[],s=new Uint8Array(Math.ceil(r/8)),c=0;for(let f=0;f>3]|=1<<(f&7);let _=Array.isArray(p)?p:typeof p[Symbol.iterator]==="function"?Array.from(p):null;if(_)for(let g of _)o.push(g)}}a[r]=o.length;let l=o.length>0?Qa(o,n.type):Ci(n.type);return A({type:e,length:r,nullCount:c,nullBitmap:c>0?s:void 0,child:l,offset:0,valueOffsets:a})}function yA(t,e){let n=e.children[0],r=e.listSize,a=t.length,o=[],s=new Uint8Array(Math.ceil(a/8)),c=0;for(let f=0;f>3]|=1<<(f&7);let _=Array.isArray(p)?p:Array.from(p);if(_.length!==r)throw TypeError(`fixed_size_list[${r}]: row ${f} has ${_.length} items`);for(let g of _)o.push(g)}let l=o.length>0?Qa(o,n.type):Ci(n.type);return A({type:e,length:a,nullCount:c,nullBitmap:c>0?s:void 0,child:l})}function _A(t,e){let n=t.length,r=e.children[0],a=r.type.children[0],o=r.type.children[1],s=new Int32Array(n+1),c=[],l=[],f=new Uint8Array(Math.ceil(n/8)),p=0;for(let I=0;I>3]|=1<<(I&7);let C=Array.isArray(x)?x:x instanceof Map?Array.from(x.entries()):typeof x[Symbol.iterator]==="function"?Array.from(x):typeof x==="object"?Object.entries(x):[];for(let[D,Y]of C)c.push(D),l.push(Y)}}s[n]=c.length;let _=c.length>0?Qa(c,a.type):Ci(a.type),g=l.length>0?Qa(l,o.type):Ci(o.type),w=A({type:r.type,length:c.length,children:[_,g],nullCount:0});return A({type:e,length:n,nullCount:p,nullBitmap:p>0?f:void 0,child:w,offset:0,valueOffsets:s})}function gA(t,e){let n=e.children,r=t.length,a=new Uint8Array(Math.ceil(r/8)),o=0,s=n.map(()=>[]);for(let l=0;l>3]|=1<<(l&7);for(let p=0;pQa(s[f],l.type));return A({type:e,length:r,nullCount:o,nullBitmap:o>0?a:void 0,children:c})}function Ci(t){if(S.isList(t))return A({type:t,length:0,nullCount:0,child:Ci(t.children[0].type),valueOffsets:new Int32Array([0])});if(S.isMap(t))return A({type:t,length:0,nullCount:0,child:Ci(t.children[0].type),valueOffsets:new Int32Array([0])});if(S.isFixedSizeList(t))return A({type:t,length:0,nullCount:0,child:Ci(t.children[0].type)});if(S.isStruct(t)){let e=t.children.map((n)=>Ci(n.type));return A({type:t,length:0,nullCount:0,children:e})}return A({type:t,length:0,nullCount:0})}function Ce(t,e,n){let r=e,a=r?.data?.[0];return wA(t,r,a,n)}function bA(t,e,n){if(typeof t?.isValid==="function")return t.isValid(n);let r=e?.nullBitmap;if(r&&r.length>0)return(r[n>>3]>>(n&7)&1)===1;return!0}function wA(t,e,n,r){if(!bA(e,n,r))return null;switch(t.typeId){case 1:return null;case 6:return e.get(r);case 2:{let o=t.bitWidth??32,s=n?.values?.[r];if(o===64)return typeof s==="bigint"?s:BigInt(s);return Number(s)}case 3:return Number(n?.values?.[r]);case 5:case 20:return String(e.get(r));case 4:case 19:case 15:return xA(e.get(r));case 7:return SA(t,n,r);case 8:{let o=n?.values?.[r];if(t.unit===vt.DAY)return Number(o);return typeof o==="bigint"?o:BigInt(o)}case 9:{let o=n?.values?.[r];if((t.bitWidth??32)===64)return typeof o==="bigint"?o:BigInt(o);return Number(o)}case 10:case 18:{let o=n?.values?.[r];return typeof o==="bigint"?o:BigInt(o)}case 13:return IA(t,e,r);case 14:return TA(t,e,n,r);case 12:case 16:return vA(t,e,n,r);case 17:return EA(t,e,n,r);case-1:return DA(t,e,n,r);default:return e.get(r)}}function xA(t){if(t==null)return null;if(t instanceof Uint8Array)return t;if(ArrayBuffer.isView(t)){let e=t;return new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}return t}function SA(t,e,n){let r=t.byteWidth??(t.bitWidth?t.bitWidth/8:16),a=e.values,o=r/4,s=n*o,c=new Uint8Array(r);for(let l=0;l>>0;c[l*4]=f&255,c[l*4+1]=f>>>8&255,c[l*4+2]=f>>>16&255,c[l*4+3]=f>>>24&255}return AA(c)}function AA(t){let e=0n;for(let n=t.length-1;n>=0;n--)e=e<<8n|BigInt(t[n]);if(t[t.length-1]&128)e-=1n<0?0:-1,c=t.typeIdToChildIndex,l=c&&c[s]!==void 0?c[s]:s,f=a[l];if(!f)return{tag:null,value:null};let p=n?.valueOffsets,_=p?p[r]:r,g=typeof e.getChildAt==="function"?e.getChildAt(l):e.children?.[l],w=g?Ce(f.type,g,_):null;return{tag:f.name,value:w}}function vA(t,e,n,r){let a=t.children[0],o=typeof e.getChildAt==="function"?e.getChildAt(0):e.children?.[0],s=n?.valueOffsets,c,l;if(s)c=s[r],l=s[r+1];else{let p=t.listSize??0;c=r*p,l=c+p}let f=[];for(let p=c;po[a.name]??null);return ue(r,n)}function ue(t,e,n="rich"){let r=e,a=r.fields.length>0?t[r.fields[0].name]?.length??0:0,o=r.fields.map((l)=>{let f=t[l.name];if(!f)return A({type:l.type,length:a,nullCount:a});let p=Ge(l.type),_=n==="raw"?p.rawToCanonical:p.richToCanonical,g=f.map((w)=>_.call(p,w));return Vl(l.type,g)}),s=new it(r.fields),c=A({type:s,length:a,children:o,nullCount:0});return new rt(r,c)}Ke();function*on(t,e="rich"){let n=t,r=n.schema.fields.map((a)=>Ge(a.type));for(let a=0;a0){if((r[e>>3]>>(e&7)&1)===0)return null}let a=Number(n.values[e]);return t.dictionary.get(a)}return t}function zl(t){if(!t)return{};let e=t;if(e.numRows===0)return{};let n={};for(let r of e.schema.fields){let a=e.getChild(r.name);if(a){let o=r.type,s=Ge(o).canonicalToRich(Ce(o,a,0)),c=r.name,l;if(r.name.startsWith("secret_")){let f=r.metadata?.get?.("secret_type");if(f)c=f,l=r.metadata?.get?.("scope")??void 0}if(s&&typeof s==="object"&&!ArrayBuffer.isView(s)){let f={...s};if(c in n)throw Error(`batchToSecretDict: duplicate secret_type '${c}' (scope=${l??"none"}). Use scoped secrets with distinct scopes to avoid collisions.`);if(n[c]=f,l)n[`${c}:${l}`]=f}else if(s===null||s===void 0);else n[c]={}}}return n}var k_=BigInt(Number.MAX_SAFE_INTEGER),BA=-k_;function us(t){if(typeof t==="bigint"){if(t>k_||t Number.MAX_SAFE_INTEGER). Keep it as a bigint.`);return Number(t)}return t}function jl(t,e=0){if(t?.type?.typeId===17&&t.valueOffsets&&t.children)return Ce(t.type,xr(t),e);return t}function fs(t,e){let n=t,r=n.numRows,a=0;for(let c=0;cp.canonicalToRich(Ce(f,l,_)))}return ue(s,n.schema)}Ke();function V_(t,e,n){for(let r of e){if(r<-1)throw Error(`${t}: unexpected negative projection ID ${r} (only -1 for row_id is allowed)`);if(r>=n)console.warn(`${t}: projection ID ${r} is out of bounds (schema has ${n} fields), ignoring`)}return e.filter((r)=>r>=0&&rn.fields[a]),n.metadata)}function ds(t,e){let n=e;if(!t)return n;let r=V_("projectBatch",t,n.schema.fields.length);if(r.length===0)return n;let a=Xa(t,n.schema),o=r.map((l)=>n.getChildAt(l).data[0]),s=new it(a.fields),c=A({type:s,length:n.numRows,children:o,nullCount:0});return new rt(a,c)}Ke();function np(t,e){let n=t;return new rt(n.schema,n.data,e)}Ke();function cr(t){let e=new an;return e.reset(void 0,t),e.close(),e.toUint8Array(!0)}function We(t){let e=Ot.from(t),n=[...e];if(n.length>0)return n[0].schema;if(e.schema)return e.schema;throw Error("Cannot deserialize schema from empty IPC stream")}function Yt(t){let e=t,n=new an;return n.reset(void 0,e.schema),n._writeRecordBatch(e),n.close(),n.toUint8Array(!0)}function fe(t){let e=Ot.from(t),n=[...e];if(n.length===0){let r=e.schema??new P([]);return An(r)}return n[0]}function ps(t){return[...Ot.from(t)]}function $l(t,e){let n=new an;n.reset(void 0,e);for(let r of t)n._writeRecordBatch(r);return n.close(),n.toUint8Array(!0)}function Hl(t,e,n){let r=ke(e,t),a=[];for(let o=0;or.richToCanonical(o));return Vl(e,a)}function rp(t){if(t.length===0){let g=new N("0",new zt,!0),w=new bi([0],[g]),I=A({type:w,length:0,typeIds:new Int8Array(0),children:[A({type:new zt,length:0,nullCount:0})]}),x=A({type:w,length:0,typeIds:new Int8Array(0),children:[A({type:new zt,length:0,nullCount:0})]});return q_([],I,x,[],[],[],[],[],w)}let e=[],n=new Map,r=[];for(let g of t){let w=CA(g.arrowType),I=n.get(w);if(I===void 0)I=e.length,n.set(w,I),e.push(g.arrowType);r.push(I)}let a=[],o=[],s=[],c=[];for(let[g,w]of e.entries()){a.push(new N(String(g),w,!0)),c.push(g);let I=t.map((C,D)=>r[D]===g?C.min:null),x=t.map((C,D)=>r[D]===g?C.max:null);o.push(Oi(I,w)),s.push(Oi(x,w))}let l=new bi(c,a),f=Int8Array.from(r),p=A({type:l,length:t.length,typeIds:f,children:o}),_=A({type:l,length:t.length,typeIds:f,children:s});return q_(t.map((g)=>g.columnName),p,_,t.map((g)=>g.hasNull),t.map((g)=>g.hasNotNull),t.map((g)=>g.distinctCount==null?null:BigInt(g.distinctCount)),t.map((g)=>g.containsUnicode),t.map((g)=>g.maxStringLength==null?null:BigInt(g.maxStringLength)),l)}function q_(t,e,n,r,a,o,s,c,l){let f=new P([new N("column_name",new Gt,!1),new N("min",l,!0),new N("max",l,!0),new N("has_null",new jt,!1),new N("has_not_null",new jt,!1),new N("distinct_count",new De,!0),new N("contains_unicode",new jt,!0),new N("max_string_length",new qn,!0)]),p=t.length,_=[Oi(t,new Gt),e,n,Oi(r,new jt),Oi(a,new jt),Oi(o,new De),Oi(s,new jt),Oi(c,new qn)],g=new it(f.fields),w=A({type:g,length:p,children:_,nullCount:0});return new rt(f,w)}var z_={name:"arrow-js"};function ip(t){return t}var{TimeUnit:op,DateUnit:RA,IntervalUnit:OA,UnionMode:NA,schema:m}=ap;function i(t,e,n,r){return Tr(t,e,n,r)}var hs=()=>Tl(),v=()=>vl();var at=()=>Ga(),Ja=()=>es(),B=()=>ns();var Yl=()=>rs();var Ni=()=>Rl(),u=()=>Ol();var d=()=>Nl(),Tn=()=>Fl();var kM={[aa.SECOND]:"s",[aa.MILLISECOND]:"ms",[aa.MICROSECOND]:"us",[aa.NANOSECOND]:"ns"};function sp(t,e){return Ul(t,e??null)}function h(t){return Ml(t)}function xt(t){return os(t)}function de(t,e,n){return Ll(t,e,n)}function et(t,e,n,r){return Pl(t,e,n,r)}var j_="vgi.cache.not_modified";var cp="vgi.status",$_="HAVE_MORE_OUTPUT",H_="CONTINUE";class Fi{positional;named;argumentsSchema;constructor(t=[],e=new Map,n=null){this.positional=t,this.named=e,this.argumentsSchema=n}get(t,e){if(typeof t==="number"){if(t=Number.MIN_SAFE_INTEGER&&e<=Number.MAX_SAFE_INTEGER)return Number(e);return e}return e}return t}function G_(t){let e=[],n={};for(let c=0;c{a.SCALAR="scalar";a.TABLE="table";a.TABLE_BUFFERING="table_buffering";a.AGGREGATE="aggregate"})(lp||={});var W_="vgi_arg",Q_="named",X_="vgi_type",J_="table",Z_="any",tg="vgi_varargs",eg="true",ng="vgi_const",rg="true",ig="vgi_doc",ag="vgi_default",og="vgi_choices",sg="vgi_range",cg="vgi_pattern";function pe(t){if(t instanceof Uint8Array)return t;if(t instanceof ArrayBuffer)return new Uint8Array(t);if(typeof Buffer<"u"&&Buffer.isBuffer(t))return new Uint8Array(t);if(t&&t.buffer instanceof ArrayBuffer)return new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return new Uint8Array(0)}var FA=new Set(["argument_names"]);function Za(t){return t.children??[]}function UA(t){let e=Za(t)[0].type;return Za(e)[1].type}function MA(t){if(Array.isArray(t))return t.map((e)=>Array.isArray(e)?[e[0],e[1]]:[e?.key,e?.value]);if(t instanceof Map)return Array.from(t.entries());if(typeof t[Symbol.iterator]==="function")return Array.from(t);return Object.entries(t)}function up(t){if(_o(t))return up(t.dictionary);if(Nu(t))return"";if(mo(t))return new Uint8Array(0);if(yo(t))return!1;if(Mr(t))return t.bitWidth===64?0n:0;if(Ou(t))return 0;if(yr(t)||_r(t))return[];if(Lr(t)){let e={};for(let n of Za(t))e[n.name]=n.nullable?null:ms(n.type,void 0,!1,n.name);return e}return null}var lg=new WeakMap;function ug(t){let e=lg.get(t);if(e!==void 0)return e;let n=yr(t)||_r(t);if(!n&&Lr(t))n=Za(t).some((r)=>ug(r.type));return lg.set(t,n),n}function ms(t,e,n,r){if(e==null){if(n)return e??null;if(yr(t)||_r(t))return[];return e}if(!ug(t))return e;if(yr(t)){let a=Za(t)[0].type,o=FA.has(r),s=Array.isArray(e)?e:Array.from(e),c=!Array.isArray(e),l=s.map((f)=>{let p=f==null&&!o?up(a):ms(a,f,!0,r);if(p!==f)c=!0;return p});return c?l:e}if(_r(t)){let a=UA(t),o=MA(e),s=!1,c=o.map(([l,f])=>{let p=f==null?up(a):ms(a,f,!0,r);if(p!==f)s=!0;return[l,p]});return s?c:e}if(Lr(t)){let a=null;for(let o of Za(t)){let s=e[o.name],c=ms(o.type,s,o.nullable,o.name);if(c!==s)(a??={...e})[o.name]=c}return a??e}return e}function Kl(t,e){let n=null;for(let r of t.fields){let a=e[r.name],o=ms(r.type,a,r.nullable,r.name);if(o!==a)(n??={...e})[r.name]=o}return n??e}function to(t){if(typeof t==="string")return new TextEncoder().encode(t);let e=pe(t);if(e.length===0&&t!=null)throw Error(`Cannot convert ${typeof t} to Uint8Array`);return e}function Gl(t,e){let n={},r=Kl(t,e);for(let a of t.fields){let o=r[a.name];if(Mr(a.type)&&a.type.bitWidth===64&&typeof o==="number")o=BigInt(o);n[a.name]=[o]}return ue(n,t)}var LA=xt([i("format",u(),!1),i("file_path",u(),!1),i("expected_schema",d(),!1)]),PA=xt([i("format",u(),!1),i("file_path",u(),!1)]),kA=m([i("function_name",u(),!1),i("arguments",d(),!1),i("function_type",et(u(),at()),!1),i("input_schema",d(),!0),i("settings",d(),!0),i("secrets",d(),!0),i("attach_opaque_data",d(),!0),i("transaction_opaque_data",d(),!0),i("resolved_secrets_provided",v(),!1),i("at_unit",u(),!0),i("at_value",u(),!0),i("copy_from",LA,!0),i("copy_to",PA,!0),i("schema_path",h(i("item",u(),!0)),!0),i("argument_names",h(i("item",u(),!0)),!0)]);function ys(t){return Gl(kA,{function_name:t.function_name,arguments:G_(t.arguments),function_type:t.function_type==null?t.function_type:String(t.function_type).toUpperCase(),input_schema:t.input_schema?cr(t.input_schema):null,settings:t.settings?Yt(t.settings):null,secrets:t.secrets?Yt(t.secrets):null,attach_opaque_data:t.attach_opaque_data??null,transaction_opaque_data:t.transaction_opaque_data??null,resolved_secrets_provided:t.resolved_secrets_provided??!1,at_unit:t.at_unit??null,at_value:t.at_value??null,copy_from:t.copy_from?{format:t.copy_from.format,file_path:t.copy_from.file_path,expected_schema:cr(t.copy_from.expected_schema)}:null,copy_to:t.copy_to?{format:t.copy_to.format,file_path:t.copy_to.file_path}:null,schema_path:t.schema_path??null,argument_names:t.argument_names??null})}function VA(t){if(t!=="UNKNOWN"&&t!=="STABLE_WITHIN_QUERY")throw Error("invalid bind repeatability");return t}function fp(t){let e=(n)=>{if(!n)return[];return(Array.isArray(n)?n:[...n]).filter((a)=>a!=null).map(String)};return{output_schema:We(to(t.output_schema)),projection_expression_profiles:e(t.projection_expression_profiles),opaque_data:t.opaque_data?to(t.opaque_data):null,lookup_secret_types:e(t.lookup_secret_types),lookup_scopes:e(t.lookup_scopes),lookup_names:e(t.lookup_names),repeatability:VA(t.repeatability)}}var zA=m([i("bind_call",d(),!1),i("output_schema",d(),!1),i("bind_opaque_data",d(),!0),i("projection_ids",h(i("item",B(),!0)),!0),i("pushdown_filters",Tn(),!0),i("join_keys",h(i("item",Tn(),!0)),!0),i("split_tokens",h(i("item",Tn(),!0)),!0),i("row_limit",B(),!0),i("phase",et(u(),at()),!0),i("finalize_state_id",d(),!0),i("execution_id",d(),!0),i("init_opaque_data",d(),!0),i("substream_id",d(),!0),i("order_by_column_name",u(),!0),i("order_by_direction",et(u(),at()),!0),i("order_by_null_order",et(u(),at()),!0),i("order_by_limit",B(),!0),i("tablesample_percentage",Ni(),!0),i("tablesample_seed",B(),!0)]),s1=m([i("execution_id",d(),!1),i("opaque_data",d(),!0),i("max_workers",B(),!1)]);function dp(t){let e=ys(t.bind_call),r={bind_call:Yt(e),output_schema:cr(t.output_schema),bind_opaque_data:t.bind_opaque_data??null,projection_ids:t.projection_ids?.map((a)=>BigInt(a))??null,pushdown_filters:t.pushdown_filters?Yt(t.pushdown_filters):null,join_keys:(t.join_keys??[]).map((a)=>Yt(a)),split_tokens:t.split_tokens??null,row_limit:t.row_limit??null,phase:t.phase??null,finalize_state_id:t.finalize_state_id??null,execution_id:t.execution_id??null,init_opaque_data:t.init_opaque_data??null,substream_id:t.substream_id??null,order_by_column_name:t.order_by_column_name??null,order_by_direction:t.order_by_direction??null,order_by_null_order:t.order_by_null_order??null,order_by_limit:t.order_by_limit??null,tablesample_percentage:t.tablesample_percentage??null,tablesample_seed:t.tablesample_seed??null};return Gl(zA,r)}function pp(t){return{execution_id:to(t.execution_id),opaque_data:t.opaque_data?to(t.opaque_data):null,max_workers:Number(t.max_workers)}}function St(t){return typeof t==="string"?[t]:Array.from(t,String)}function _s(t,e){return Yt(ke([HA(t,Kl(t,e))],t))}function fg(t,e){let n=Math.min(t.length,e.length);for(let r=0;r=55296&&a<=57343,c=o>=55296&&o<=57343;if(s!==c)return s?1:-1;return a-o}return t.length-e.length}function jA(t){if(Array.isArray(t))return t.map((e)=>Array.isArray(e)?[e[0],e[1]]:[e?.key,e?.value]);if(t instanceof Map)return Array.from(t.entries());if(typeof t[Symbol.iterator]==="function")return Array.from(t);return Object.entries(t)}function $A(t,e){if(typeof t==="string"&&typeof e==="string")return fg(t,e);if((typeof t==="number"||typeof t==="bigint")&&(typeof e==="number"||typeof e==="bigint"))return te?1:0;return fg(String(t),String(e))}var dg=new WeakMap;function hp(t){let e=dg.get(t);if(e!==void 0)return e;let n=_r(t);if(!n&&(yr(t)||Lr(t)))n=t.children.some((r)=>hp(r.type));return dg.set(t,n),n}function Wl(t,e){if(e==null||!hp(t))return e;if(_r(t)){let r=t.children[0].type.children[1].type;return jA(e).map(([a,o])=>[a,Wl(r,o)]).sort((a,o)=>$A(a[0],o[0]))}if(yr(t)){let n=t.children[0].type;return Array.from(e,(r)=>Wl(n,r))}if(Lr(t)){let n={...e};for(let r of t.children)n[r.name]=Wl(r.type,e[r.name]);return n}return e}function HA(t,e){let n=null;for(let r of t.fields){if(!hp(r.type))continue;let a=e[r.name];if(a==null)continue;n??={...e},n[r.name]=Wl(r.type,a)}return n??e}function lr(t,e){let n=fe(e);if(n.numRows===0){let a=t.fields.map((o)=>o.name).join(",");throw Error(`decodeASD: empty batch (expected 1 row for ${a})`)}let r={};for(let a of t.fields){let o=n.getChild(a.name),s=o?o.get(0):null;r[a.name]=eo(s,a.type)}return r}function eo(t,e){if(t==null)return null;if(yr(e)){let n=e.children[0].type,r=[];for(let a of t)r.push(eo(a,n));return r}if(_r(e)){let r=e.children[0].type.children[1].type,a={},o=(s,c)=>{Object.defineProperty(a,String(s),{value:eo(c,r),enumerable:!0,writable:!0,configurable:!0})};if(t[Symbol.iterator]){for(let s of t)if(Array.isArray(s))o(s[0],s[1]);else if(s&&typeof s==="object")o(s.key??s[0],s.value??s[1])}else if(typeof t==="object")for(let[s,c]of Object.entries(t))o(s,c);return a}if(Lr(e)){let n=e.children,r={};for(let a of n)r[a.name]=eo(t[a.name],a.type);return r}if(_o(e))return eo(t,e.dictionary);if(mo(e))return YA(t);if(Mr(e)&&e.bitWidth===64){if(typeof t==="bigint"){if(t>=BigInt(Number.MIN_SAFE_INTEGER)&&t<=BigInt(Number.MAX_SAFE_INTEGER))return Number(t);return t}return t}if(yo(e))return Boolean(t);return t}function YA(t){if(t instanceof Uint8Array)return t;if(t instanceof ArrayBuffer)return new Uint8Array(t);if(t&&t.buffer instanceof ArrayBuffer)return new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return new Uint8Array(0)}function ca(t,e){if(e==null)return[];let n=[],r=ps(e),a=(o)=>{if(o.fields.length!==t.fields.length||t.fields.some((s,c)=>{let l=o.fields[c];return l.name!==s.name||l.nullable!==s.nullable||si(l.type)!==si(s.type)}))throw Error("catalog collection has an incompatible metadata schema")};if(r.length===0)a(We(e));for(let o of r){a(o.schema);for(let s=0;slr(nu,t);var bg=(t)=>lr(_g,t);var mp=(t)=>lr(pg,t);var yp=(t)=>lr(Zl,t);var _p=(t)=>lr(eu,t);var ru=(t)=>lr(tu,t);var iu=(t)=>lr(Ql,t);var au=(t)=>lr(Xl,t);var ou=(t)=>lr(Jl,t);function wg(t){let e=t.arguments,n=[],r={};if(e!=null){let s=fe(pe(e));for(let c of s.schema.fields){let l=s.getChild(c.name),f=l?l.get(0):null;if(c.name.startsWith("arg_")){let p=parseInt(c.name.slice(4),10);while(n.length<=p)n.push(null);n[p]=f}else r[c.name]=f}}let a=t.required_extensions,o=a==null?[]:(Array.isArray(a)?a:[...a]).filter((s)=>s!=null).map(String);return{functionName:String(t.function_name??""),positionalArguments:n,namedArguments:r,requiredExtensions:o,schemaPath:t.schema_path==null?null:(Array.isArray(t.schema_path)?t.schema_path:[...t.schema_path]).filter((s)=>s!=null).map(String)}}function gs(t){return{request:Yt(t)}}function Re(t){let e=pe(t.result),n=fe(e);return Ri(n)}function II(t){if(t===null)return hs();if(typeof t==="string")return u();if(typeof t==="bigint")return B();if(typeof t==="number"){if(!Number.isFinite(t))throw TypeError(`Attach option number values must be finite (got ${t}). Use a bigint for integers or a finite number for floats.`);return Ni()}if(typeof t==="boolean")return v();if(t instanceof Uint8Array)return d();throw TypeError(`Unsupported attach option value type: ${typeof t}. Supported: string, number, bigint, boolean, null, Uint8Array.`)}function gp(t){if(!t)return null;let e=Object.entries(t);if(e.length===0)return null;let n=[],r={};for(let[s,c]of e){let l=II(c);n.push(i(s,l,!0)),r[s]=[c]}let a=m(n),o=ue(r,a);return Yt(o)}Gp();class kt extends Error{remoteTraceback;errorType;constructor(t,e){super(t,e?.cause!=null?{cause:e.cause}:void 0);if(this.name="VgiClientError",e?.remoteTraceback)this.remoteTraceback=e.remoteTraceback;if(e?.errorType)this.errorType=e.errorType}static fromRpcError(t){let e=`${t.errorType}: ${t.errorMessage}`,n=t.remoteTraceback?`${e} Remote traceback: -${$.remoteTraceback}`:Z;return new d$(Q,{remoteTraceback:$.remoteTraceback||void 0,errorType:$.errorType,cause:$})}}function uB($){return{async call(Z,Q){try{return await $.call(Z,Q)}catch(X){if(X instanceof b)throw d$.fromRpcError(X);throw X}},async stream(Z,Q){try{return await $.stream(Z,Q)}catch(X){if(X instanceof b)throw d$.fromRpcError(X);throw X}},async callRaw(Z,Q){try{return await $.callRaw(Z,Q)}catch(X){if(X instanceof b)throw d$.fromRpcError(X);throw X}},async streamRaw(Z,Q,X){try{return await $.streamRaw(Z,Q,X)}catch(J){if(J instanceof b)throw d$.fromRpcError(J);throw J}},describe:()=>$.describe(),close:()=>$.close()}}function UX($,Z){if(!$)return[];return(Array.isArray($)?$:[...$]).filter((X)=>X!=null).map((X)=>Z(K8(X)))}function pB($){let Z={};if(!$)return Z;let Q=$;if(typeof Q[Symbol.iterator]!=="function")return Z;for(let X of Q)if(Array.isArray(X))Z[String(X[0])]=String(X[1]);else if(X&&typeof X==="object"){let J=X;Z[String(J.key??J[0]??"")]=String(J.value??J[1]??"")}return Z}function d5($){if(Symbol.asyncIterator in $)return $[Symbol.asyncIterator]();let Z=$[Symbol.iterator]();return{async next(){return Z.next()}}}class z9{rpc;defaultAttachOpaqueData;constructor($,Z){this.rpc=uB($),this.defaultAttachOpaqueData=Z?.attachOpaqueData??null}static async fromIroh($,Z={},Q,X){if(!/^(iroh|httpi):\/\//.test($))throw TypeError("VgiClient.fromIroh requires an iroh:// or httpi:// endpoint");let J=X;if(!J){let W=await Promise.resolve().then(() => (ez(),KA));J=$.startsWith("iroh://")?W.irohConnect:W.httpiConnect}if(!J)throw TypeError("no Iroh connector is available in this runtime");return new z9(await J($,Z),Q)}async _doBind($,Z,Q,X,J,W,Y,G=null,H){let z={function_name:$,arguments:Q,function_type:Z,input_schema:X,settings:J??null,secrets:W??null,attach_opaque_data:G??this.defaultAttachOpaqueData,transaction_opaque_data:Y??null,resolved_secrets_provided:!1},q=dA(z),P=await this.rpc.call("bind",l5(q));if(!P)throw new d$("bind returned null");let U=pZ(P),M=VB(U);if(H)H(M);return{request:z,response:M}}async _doInit($,Z,Q){let X=Q?.orderBy??null,J=Q?.tablesample??null,W={bind_call:$,output_schema:Z.output_schema,bind_opaque_data:Z.opaque_data,split_tokens:null,row_limit:null,projection_ids:Q?.projectionIds??null,pushdown_filters:Q?.pushdownFilters??null,join_keys:Q?.joinKeys??[],phase:Q?.phase??null,finalize_state_id:null,order_by_column_name:X?.columnName??null,order_by_direction:X?.direction??null,order_by_null_order:X?.nullOrder??null,order_by_limit:X?.limit==null?null:BigInt(X.limit),tablesample_percentage:J?.percentage??null,tablesample_seed:J?.seed==null?null:BigInt(J.seed),execution_id:Q?.executionId??null,init_opaque_data:null,substream_id:null},Y=EB(W),G=await this.rpc.stream("init",l5(Y)),H=G.header;if(!H)throw new d$("init did not return a header");let z=wB(H);return{session:G,initResponse:z}}async*tableFunction($){let Z=$.arguments??new h6,{request:Q,response:X}=await this._doBind($.functionName,"table",Z,null,$.settings??null,null,$.transactionOpaqueData??null,$.attachOpaqueData??null,$.onBind??null),{session:J}=await this._doInit(Q,X,{projectionIds:$.projectionIds,pushdownFilters:$.pushdownFilters,orderBy:$.orderBy,tablesample:$.tablesample,joinKeys:$.joinKeys}),W=X.output_schema;try{for await(let Y of J)if(Y.length>0)yield fJ(Y,W)}finally{J.close()}}async*scalarFunctionRows($){let Z=d5($.input),Q=await Z.next();if(Q.done)throw new d$(`scalarFunction(${$.functionName}): input iterator yielded no batches; at least one batch is required to determine the input schema`);let X=Q.value,J=X.schema,W=$.arguments??new h6,{request:Y,response:G}=await this._doBind($.functionName,"scalar",W,J,$.settings??null,$.secrets??null,$.transactionOpaqueData??null,$.attachOpaqueData??null,$.onBind??null),{session:H}=await this._doInit(Y,G);try{let z=await H.exchange([...y8(X)]);if(z.length>0)yield z;for await(let q of{[Symbol.asyncIterator]:()=>Z}){let P=await H.exchange([...y8(q)]);if(P.length>0)yield P}}finally{H.close()}}async*scalarFunction($){let Z=d5($.input),Q=await Z.next();if(Q.done)throw new d$(`scalarFunction(${$.functionName}): input iterator yielded no batches; at least one batch is required to determine the input schema`);let X=Q.value,J=X.schema,W=$.arguments??new h6,{request:Y,response:G}=await this._doBind($.functionName,"scalar",W,J,$.settings??null,$.secrets??null,$.transactionOpaqueData??null,$.attachOpaqueData??null,$.onBind??null),{session:H}=await this._doInit(Y,G),z=G.output_schema;try{let q=await H.exchange([...y8(X)]);if(q.length>0)yield fJ(q,z);for await(let P of{[Symbol.asyncIterator]:()=>Z}){let U=await H.exchange([...y8(P)]);if(U.length>0)yield fJ(U,z)}}finally{H.close()}}async*tableInOutFunctionRows($){let Z=d5($.input),Q=await Z.next();if(Q.done)throw new d$(`tableInOutFunction(${$.functionName}): input iterator yielded no batches; at least one batch is required to determine the input schema`);let X=Q.value,J=X.schema,W=$.arguments??new h6,{request:Y,response:G}=await this._doBind($.functionName,"table",W,J,$.settings??null,null,$.transactionOpaqueData??null,$.attachOpaqueData??null,$.onBind??null),{session:H,initResponse:z}=await this._doInit(Y,G,{projectionIds:$.projectionIds,pushdownFilters:$.pushdownFilters,phase:"INPUT",orderBy:$.orderBy,tablesample:$.tablesample,joinKeys:$.joinKeys});try{let P=await H.exchange([...y8(X)]);if(P.length>0)yield P;for await(let U of{[Symbol.asyncIterator]:()=>Z}){let M=await H.exchange([...y8(U)]);if(M.length>0)yield M}}finally{H.close()}let{session:q}=await this._doInit(Y,G,{projectionIds:$.projectionIds,pushdownFilters:$.pushdownFilters,phase:"FINALIZE",executionId:z.execution_id,orderBy:$.orderBy,tablesample:$.tablesample,joinKeys:$.joinKeys});try{for await(let P of q)if(P.length>0)yield P}finally{q.close()}}async*tableInOutFunction($){let Z=d5($.input),Q=await Z.next();if(Q.done)throw new d$(`tableInOutFunction(${$.functionName}): input iterator yielded no batches; at least one batch is required to determine the input schema`);let X=Q.value,J=X.schema,W=$.arguments??new h6,{request:Y,response:G}=await this._doBind($.functionName,"table",W,J,$.settings??null,null,$.transactionOpaqueData??null,$.attachOpaqueData??null,$.onBind??null),{session:H,initResponse:z}=await this._doInit(Y,G,{projectionIds:$.projectionIds,pushdownFilters:$.pushdownFilters,phase:"INPUT",orderBy:$.orderBy,tablesample:$.tablesample,joinKeys:$.joinKeys}),q=G.output_schema;try{let U=await H.exchange([...y8(X)]);if(U.length>0)yield fJ(U,q);for await(let M of{[Symbol.asyncIterator]:()=>Z}){let L=await H.exchange([...y8(M)]);if(L.length>0)yield fJ(L,q)}}finally{H.close()}let{session:P}=await this._doInit(Y,G,{projectionIds:$.projectionIds,pushdownFilters:$.pushdownFilters,phase:"FINALIZE",executionId:z.execution_id,orderBy:$.orderBy,tablesample:$.tablesample,joinKeys:$.joinKeys});try{for await(let U of P)if(U.length>0)yield fJ(U,q)}finally{P.close()}}async*tableFunctionRows($){let Z=$.arguments??new h6,{request:Q,response:X}=await this._doBind($.functionName,"table",Z,null,$.settings??null,null,$.transactionOpaqueData??null,$.attachOpaqueData??null,$.onBind??null),{session:J}=await this._doInit(Q,X,{projectionIds:$.projectionIds,pushdownFilters:$.pushdownFilters,orderBy:$.orderBy,tablesample:$.tablesample,joinKeys:$.joinKeys});try{for await(let W of J)if(W.length>0)yield W}finally{J.close()}}async catalogsInfo(){let $=await this.rpc.call("catalog_catalogs",{});if(!$)return[];let Z=pZ($);return UX(Z.items,hB)}async catalogs(){return(await this.catalogsInfo()).map((Z)=>Z.name)}async catalogAttach($,Z){if(Z?.options!=null&&Z?.optionsBytes!=null)throw new d$("catalogAttach: cannot specify both `options` and `optionsBytes`");let Q;if(Z?.optionsBytes!=null)Q=Z.optionsBytes.byteLength===0?null:Z.optionsBytes;else Q=DA(Z?.options);let X=C([K("name",F(),!1),K("options",N(),!0),K("data_version_spec",F(),!0),K("implementation_version",F(),!0),K("client_capabilities",N(),!0)]),J=lJ({name:[$],options:[Q],data_version_spec:[Z?.dataVersionSpec??null],implementation_version:[Z?.implementationVersion??null],client_capabilities:[Z?.clientCapabilities?SB(vB,Z.clientCapabilities):null]},X),W=await this.rpc.call("catalog_attach",l5(J));if(!W)throw new d$("catalog_attach returned null");let Y=pZ(W);return{attach_opaque_data:K8(Y.attach_opaque_data),supports_transactions:Y.supports_transactions??!1,supports_time_travel:Y.supports_time_travel??!1,catalog_version_frozen:Y.catalog_version_frozen??!1,catalog_version:Number(Y.catalog_version??0),attach_opaque_data_required:Y.attach_opaque_data_required??!0,default_schema:Y.default_schema??"main",settings:Y.settings?(Array.isArray(Y.settings)?Y.settings:[...Y.settings]).map(K8):[],secret_types:Y.secret_types?(Array.isArray(Y.secret_types)?Y.secret_types:[...Y.secret_types]).map(K8):[],attach_catalogs:Y.attach_catalogs?(Array.isArray(Y.attach_catalogs)?Y.attach_catalogs:[...Y.attach_catalogs]).map(K8):[],comment:Y.comment??null,tags:pB(Y.tags),supports_column_statistics:Y.supports_column_statistics??!1,global_functions:Y.global_functions?(Array.isArray(Y.global_functions)?Y.global_functions:[...Y.global_functions]).map(K8):[],global_function_prefix:Y.global_function_prefix??"",resolved_data_version:Y.resolved_data_version??null,resolved_implementation_version:Y.resolved_implementation_version??null}}async catalogDetach($){await this.rpc.call("catalog_detach",{attach_opaque_data:$})}async catalogCreate($,Z,Q){let X;if(Q==null)X=null;else if(Q instanceof Uint8Array)X=Q.byteLength===0?null:Q;else{if(Q.options!=null&&Q.optionsBytes!=null)throw new d$("catalogCreate: cannot specify both `options` and `optionsBytes`");if(Q.optionsBytes!=null)X=Q.optionsBytes.byteLength===0?null:Q.optionsBytes;else X=DA(Q.options)}await this.rpc.call("catalog_create",{name:$,on_conflict:Z,options:X})}async catalogDrop($){await this.rpc.call("catalog_drop",{name:$})}async catalogVersion($,Z){let Q=await this.rpc.call("catalog_version",{attach_opaque_data:$,transaction_opaque_data:Z??null});if(!Q)throw new d$("catalog_version returned null");let X=pZ(Q);return Number(X.version??0)}async transactionBegin($){let Z=await this.rpc.call("catalog_transaction_begin",{attach_opaque_data:$});if(!Z)throw new d$("transaction_begin returned null");let Q=pZ(Z);if(!Q.transaction_opaque_data)throw new d$("transaction_begin returned no transaction_opaque_data");return K8(Q.transaction_opaque_data)}async transactionCommit($,Z){await this.rpc.call("catalog_transaction_commit",{attach_opaque_data:$,transaction_opaque_data:Z})}async transactionRollback($,Z){await this.rpc.call("catalog_transaction_rollback",{attach_opaque_data:$,transaction_opaque_data:Z})}async schemas($,Z){let Q=await this.rpc.call("catalog_schemas",{attach_opaque_data:$,transaction_opaque_data:Z??null});if(!Q)return[];let X=pZ(Q);return UX(X.items,MA)}async schemaGet($,Z,Q){let X=await this.rpc.call("catalog_schema_get",{attach_opaque_data:$,path:W$(Z),transaction_opaque_data:Q??null});if(!X)return null;let J=pZ(X),W=UX(J.items,MA);return W.length>0?W[0]:null}async schemaCreate($,Z,Q){let X=Q?.tags?new Map(Object.entries(Q.tags)):null;await this.rpc.call("catalog_schema_create",{attach_opaque_data:$,path:W$(Z),on_conflict:Q?.onConflict??"error",comment:Q?.comment??null,tags:X,transaction_opaque_data:Q?.transactionOpaqueData??null})}async schemaDrop($,Z,Q,X,J){await this.rpc.call("catalog_schema_drop",{attach_opaque_data:$,path:W$(Z),ignore_not_found:Q??!1,cascade:X??!1,transaction_opaque_data:J??null})}async schemaContentsTables($,Z,Q){let X=await this.rpc.call("catalog_schema_contents_tables",{attach_opaque_data:$,path:W$(Z),transaction_opaque_data:Q??null});if(!X)return[];let J=pZ(X);return UX(J.items,LA)}async schemaContentsViews($,Z,Q){let X=await this.rpc.call("catalog_schema_contents_views",{attach_opaque_data:$,path:W$(Z),transaction_opaque_data:Q??null});if(!X)return[];let J=pZ(X);return UX(J.items,AA)}async schemaContentsFunctions($,Z,Q,X){let J=await this.rpc.call("catalog_schema_contents_functions",{attach_opaque_data:$,path:W$(Z),type:Q,transaction_opaque_data:X??null});if(!J)return[];let W=pZ(J);return UX(W.items,bB)}async tableGet($,Z,Q,X){let J=await this.rpc.call("catalog_table_get",{attach_opaque_data:$,schema_path:W$(Z),name:Q,transaction_opaque_data:X??null});if(!J)return null;let W=pZ(J),Y=UX(W.items,LA);return Y.length>0?Y[0]:null}async tableCreate($,Z,Q,X,J,W,Y,G,H){await this.rpc.call("catalog_table_create",{attach_opaque_data:$,schema_path:W$(Z),name:Q,columns:X,on_conflict:J,not_null_constraints:W??null,unique_constraints:Y??null,check_constraints:G??null,transaction_opaque_data:H??null})}async tableDrop($,Z,Q,X,J,W){await this.rpc.call("catalog_table_drop",{attach_opaque_data:$,schema_path:W$(Z),name:Q,ignore_not_found:X??!1,cascade:J??!1,transaction_opaque_data:W??null})}async tableScanFunctionGet($,Z,Q,X,J,W){let Y=await this.rpc.call("catalog_table_scan_function_get",{attach_opaque_data:$,schema_path:W$(Z),name:Q,at_unit:X??null,at_value:J??null,transaction_opaque_data:W??null});if(!Y)throw new d$("table_scan_function_get returned null");return mB(pZ(Y))}async tableCommentSet($,Z,Q,X,J,W){await this.rpc.call("catalog_table_comment_set",{attach_opaque_data:$,schema_path:W$(Z),name:Q,comment:X??null,ignore_not_found:J??null,transaction_opaque_data:W??null})}async tableRename($,Z,Q,X,J,W){await this.rpc.call("catalog_table_rename",{attach_opaque_data:$,schema_path:W$(Z),name:Q,new_name:X,ignore_not_found:J??null,transaction_opaque_data:W??null})}async tableColumnAdd($,Z,Q,X,J,W,Y,G){await this.rpc.call("catalog_table_column_add",{attach_opaque_data:$,schema_path:W$(Z),name:Q,column_name:X,column_type:J,default_value:W??null,ignore_not_found:Y??null,transaction_opaque_data:G??null})}async tableColumnDrop($,Z,Q,X,J,W){await this.rpc.call("catalog_table_column_drop",{attach_opaque_data:$,schema_path:W$(Z),name:Q,column_name:X,ignore_not_found:J??null,transaction_opaque_data:W??null})}async tableColumnRename($,Z,Q,X,J,W,Y){await this.rpc.call("catalog_table_column_rename",{attach_opaque_data:$,schema_path:W$(Z),name:Q,column_name:X,new_name:J,ignore_not_found:W??null,transaction_opaque_data:Y??null})}async tableColumnDefaultSet($,Z,Q,X,J,W,Y){await this.rpc.call("catalog_table_column_default_set",{attach_opaque_data:$,schema_path:W$(Z),name:Q,column_name:X,default_value:J,ignore_not_found:W??null,transaction_opaque_data:Y??null})}async tableColumnDefaultDrop($,Z,Q,X,J,W){await this.rpc.call("catalog_table_column_default_drop",{attach_opaque_data:$,schema_path:W$(Z),name:Q,column_name:X,ignore_not_found:J??null,transaction_opaque_data:W??null})}async tableColumnTypeChange($,Z,Q,X,J,W,Y){await this.rpc.call("catalog_table_column_type_change",{attach_opaque_data:$,schema_path:W$(Z),name:Q,column_definition:X,expression:J??null,ignore_not_found:W??!1,transaction_opaque_data:Y??null})}async tableNotNullSet($,Z,Q,X,J,W){await this.rpc.call("catalog_table_not_null_set",{attach_opaque_data:$,schema_path:W$(Z),name:Q,column_name:X,ignore_not_found:J??null,transaction_opaque_data:W??null})}async tableNotNullDrop($,Z,Q,X,J,W){await this.rpc.call("catalog_table_not_null_drop",{attach_opaque_data:$,schema_path:W$(Z),name:Q,column_name:X,ignore_not_found:J??null,transaction_opaque_data:W??null})}async viewGet($,Z,Q,X){let J=await this.rpc.call("catalog_view_get",{attach_opaque_data:$,schema_path:W$(Z),name:Q,transaction_opaque_data:X??null});if(!J)return null;let W=pZ(J),Y=UX(W.items,AA);return Y.length>0?Y[0]:null}async viewCreate($,Z,Q,X,J,W){await this.rpc.call("catalog_view_create",{attach_opaque_data:$,schema_path:W$(Z),name:Q,definition:X,on_conflict:J,transaction_opaque_data:W??null})}async viewDrop($,Z,Q,X,J,W){await this.rpc.call("catalog_view_drop",{attach_opaque_data:$,schema_path:W$(Z),name:Q,ignore_not_found:X??!1,cascade:J??!1,transaction_opaque_data:W??null})}async viewRename($,Z,Q,X,J,W){await this.rpc.call("catalog_view_rename",{attach_opaque_data:$,schema_path:W$(Z),name:Q,new_name:X,ignore_not_found:J??null,transaction_opaque_data:W??null})}async viewCommentSet($,Z,Q,X,J,W){await this.rpc.call("catalog_view_comment_set",{attach_opaque_data:$,schema_path:W$(Z),name:Q,comment:X??null,ignore_not_found:J??null,transaction_opaque_data:W??null})}async macroGet($,Z,Q,X){let J=await this.rpc.call("catalog_macro_get",{attach_opaque_data:$,schema_path:W$(Z),name:Q,transaction_opaque_data:X??null});if(!J)return null;let W=pZ(J),Y=UX(W.items,UA);return Y.length>0?Y[0]:null}async schemaContentsMacros($,Z,Q,X){let J=await this.rpc.call("catalog_schema_contents_macros",{attach_opaque_data:$,path:W$(Z),type:Q,transaction_opaque_data:X??null});if(!J)return[];let W=pZ(J);return UX(W.items,UA)}async macroCreate($,Z,Q,X,J,W,Y,G,H,z){let q=lJ({attach_opaque_data:[$],schema_path:[W$(Z)],name:[Q],macro_type:[X],parameters:[J],definition:[W],on_conflict:[Y],parameter_default_values:[G??null],arguments_schema:[H??null],transaction_opaque_data:[z??null]},yB);await this.rpc.call("catalog_macro_create",l5(q))}async macroDrop($,Z,Q,X,J){await this.rpc.call("catalog_macro_drop",{attach_opaque_data:$,schema_path:W$(Z),name:Q,ignore_not_found:X??!1,transaction_opaque_data:J??null})}close(){this.rpc.close()}}function cB($){let Z=[],Q=0;for(let X of $.fields){let J=X.metadata,Y=J.get(YB)===GB?X.name:Q++,G=J.get(KB),H=G===HB,z=G===zB,q=J.get(qB)===PB,P=J.get(FB)===NB,U=J.get(UB)??"",M=J.get(MB),L=J.get(LB),D=J.get(AB),R=J.get(DB);Z.push({name:X.name,position:Y,arrowType:X.type,isTableInput:H,isAnyType:z,isVarargs:q,isConst:P,doc:U,defaultJson:M,choicesJson:L,rangeNotation:D,pattern:R})}return Z}var ks=C([K("name",F(),!1),K("description",F(),!1),K("type",N(),!1),K("default_value",N(),!0),K("required",Z0(),!0)]);function lB($){let Z=[...y8(GY($))][0];if(Z===void 0)throw Error("Cannot deserialize AttachOptionSpec from an empty batch");let X=a5(K8(Z.type)).fields[0].type,J,W=Z.default_value;if(W!=null){let Y=K8(W);if(Y.length>0)J=[...y8(GY(Y))][0]?.value}return{name:String(Z.name),description:String(Z.description??""),type:X,...J===void 0||J===null?{}:{default:J},required:Z.required===!0}}function dB($){return Array.from($,lB)}R7();export{cB as schemaToArgumentSpecs,V7 as httpConnect,a5 as deserializeSchema,dB as deserializeAttachOptionSpecs,d$ as VgiClientError,z9 as VgiClient,Q$ as TypeId,cA as FunctionType}; +${t.remoteTraceback}`:e;return new kt(n,{remoteTraceback:t.remoteTraceback||void 0,errorType:t.errorType,cause:t})}}function Rb(t){return{async call(e,n){try{return await t.call(e,n)}catch(r){if(r instanceof k)throw kt.fromRpcError(r);throw r}},async stream(e,n){try{return await t.stream(e,n)}catch(r){if(r instanceof k)throw kt.fromRpcError(r);throw r}},async callRaw(e,n){try{return await t.callRaw(e,n)}catch(r){if(r instanceof k)throw kt.fromRpcError(r);throw r}},async streamRaw(e,n,r){try{return await t.streamRaw(e,n,r)}catch(a){if(a instanceof k)throw kt.fromRpcError(a);throw a}},describe:()=>t.describe(),close:()=>t.close()}}var Eu="vgi.column.generated.v1",Wp="vgi.column.generation.v1",vT="vgi.expression.duckdb.scalar.v1",Nb=8388608,ET=(t)=>btoa(Array.from(t,(e)=>String.fromCharCode(e)).join(""));function Du(t,e){if(typeof t!=="string"||!t||t.includes("\x00")||new TextEncoder().encode(t).length>e)throw Error("invalid generated expression text");for(let n=0;n=55296&&r<=56319){let a=t.charCodeAt(++n);if(!(a>=56320&&a<=57343))throw Error("invalid generated expression Unicode")}else if(r>=56320&&r<=57343)throw Error("invalid generated expression Unicode")}}function DT(t,e){if(Du(t.sql,1048576),!t.sql.trim()||t.profile!==vT)throw Error("unsupported generated expression profile or empty SQL");if(!Array.isArray(t.schema_path)||t.schema_path.length>32)throw Error("invalid generated expression owner");if(t.schema_path.forEach((r)=>Du(r,4096)),JSON.stringify(t.schema_path)!==JSON.stringify(e))throw Error("generated expression owner differs from table schema");let n=t.evaluation_context;if(!n)throw Error("generated expression requires explicit evaluation context");if(Du(n.timezone,256),Du(n.collation,256),!["asc","desc"].includes(n.default_order)||!["nulls_first","nulls_last","nulls_first_on_asc_last_on_desc","nulls_last_on_asc_first_on_desc"].includes(n.default_null_order)||typeof n.preserve_insertion_order!=="boolean")throw Error("invalid generated expression evaluation context")}function BT(t){if(!t.length||t.length>Nb)throw Error("invalid generated expression size");let e=new DataView(t.buffer,t.byteOffset,t.byteLength),n=[],r=[],a=0;while(at.length)throw Error("invalid expression IPC length");let c=a+e.getUint32(a,!0),l=c-e.getInt32(c,!0),f=(w)=>4+w*2t.length-a-s)throw Error("invalid expression IPC body");if(e.getUint8(c+p)===3){let w=f(2),I=c+w+e.getUint32(c+w,!0),x=I-e.getInt32(I,!0),C=(M)=>4+M*2g)throw Error("invalid expression string buffer");r.push(t.subarray(a+s+lt,a+s+lt+Z))}}a+=s+g}throw Error("expression IPC lacks end marker")}function Ob(t){let e=(n)=>[n.name,n.nullable,String(n.type),(n.type.children??[]).map(e)];return JSON.stringify(t.fields.map(e))}function CT(t,e){if(!t.length||t.length>4*Math.ceil(Nb/3))throw Error("invalid generated expression size");let n=Uint8Array.from(atob(t),(s)=>s.charCodeAt(0));if(ET(n)!==t)throw Error("generated expression requires canonical base64");let r=BT(n),a=fe(n);if(a.numRows!==1||Ob(a.schema)!==Ob(nu))throw Error("invalid SqlExpression record schema");let o=new TextDecoder("utf-8",{fatal:!0});r.forEach((s)=>o.decode(s)),DT(gg(n),e)}function RT(t,e,n=!0){let r=(o)=>{for(let s of o?.children??o?.fields??[])a(s);if(o?.dictionary)r(o.dictionary)},a=(o)=>{let s=o.metadata??new Map;for(let c of s.keys())if(c.startsWith("vgi.column.generated.")&&c!==Eu||c.startsWith("vgi.column.generation.")&&c!==Wp)throw Error("unknown generated metadata version");if(s.has("generated_expression"))throw Error("legacy generated expression requires explicit canonical translation");if(s.has(Eu)!==s.has(Wp))throw Error("generated expression and category must occur together");if(s.has(Eu)){if(s.get(Wp)!=="virtual")throw Error("only VIRTUAL generated columns are supported");if(s.has("default")||s.has("vgi.column.default.v1"))throw Error("generated column cannot have a default");if(CT(s.get(Eu),e),!n)throw Error("generated columns unsupported by this catalog")}r(o.type)};t.fields.forEach(a)}function Fb(t,e,n=!0){RT(We(t),e,n)}function Ub(t){for(let e=0;e=55296&&n<=56319){let r=t.charCodeAt(++e);if(!(r>=56320&&r<=57343))throw Error("column metadata contains an unpaired surrogate")}else if(n>=56320&&n<=57343)throw Error("column metadata contains an unpaired surrogate")}}function OT(t){let e=0,n=()=>{while(e{n();let o=e;if(t[e++]!=='"')throw Error("column tags require string keys and values");while(et.length)throw Error("invalid column schema IPC");let a=t.subarray(n,n+r),o=new DataView(a.buffer,a.byteOffset,a.byteLength),s=(D)=>o.getUint32(D,!0),c=(D)=>o.getInt32(D,!0),l=(D)=>o.getUint16(D,!0),f=(D,Y)=>{let M=D-c(D),nt=4+Y*2;return nt{let M=f(D,Y);return M?D+M+s(D+M):0},_=(D,Y,M)=>{let nt=p(D,Y);if(!nt)return;let lt=s(nt);if(lt>(a.length-nt-4)/4)throw Error("invalid column metadata vector");for(let Z=0;Z{let M=p(D,Y);if(!M)return"";let nt=s(M);if(M+4+nt>a.length)throw Error("invalid column metadata string");return new TextDecoder("utf-8",{fatal:!0}).decode(a.subarray(M+4,M+4+nt))},w=(D,Y)=>{if(Y>64)throw Error("column metadata nesting too deep");let M=new Set;_(D,6,(nt)=>{let lt=g(nt,0);if(M.has(lt))throw Error("duplicate column metadata key");if(M.add(lt),lt==="comment"||lt==="tags")NT(lt,g(nt,1))}),_(D,5,(nt)=>w(nt,Y+1))},I=s(0),x=f(I,1);if(!x||o.getUint8(I+x)!==1)throw Error("expected column schema message");let C=p(I,2);if(!C)throw Error("missing column schema");_(C,1,(D)=>w(D,0))}function Mb(t){if(t.columns.length)FT(t.columns),Fb(t.columns,t.schema_path)}var Qp=(t)=>JSON.stringify(t.map((e)=>e.toLowerCase())),UT=["schemas","tables","views","functions","macros","indexes"];function Lb(t,e){if(t.not_modified){if(t.etag==null||e!==void 0&&t.etag!==e||UT.some((s)=>t[s]!=null))throw Error("invalid catalog_contents not_modified response");return[]}let n=ca(Ql,t.schemas),r=[],a=new Map;for(let s of n){if(!Array.isArray(s.path)||s.path.length===0)throw Error("catalog schema has an empty path");let c=Qp(s.path);if(a.has(c))throw Error("catalog contains duplicate schema paths");if(s.path.length>1&&!a.has(Qp(s.path.slice(0,-1))))throw Error("catalog schema is missing its preceding parent");let l={schema:s,tables:[],views:[],scalar_functions:[],aggregate_functions:[],table_functions:[],scalar_macros:[],table_macros:[],indexes:[]};a.set(c,l),r.push(l)}let o=(s)=>{let c=a.get(Qp(s.schema_path));if(!c)throw Error("catalog object references an absent schema");return c};for(let s of ca(Xl,t.tables))Mb(s),o(s).tables.push(s);for(let s of ca(Jl,t.views))o(s).views.push(s);for(let s of ca(Zl,t.functions)){let c=o(s);switch(String(s.function_type).toUpperCase()){case"SCALAR":c.scalar_functions.push(s);break;case"AGGREGATE":c.aggregate_functions.push(s);break;case"TABLE":case"TABLE_BUFFERING":c.table_functions.push(s);break;default:throw Error(`unknown catalog function type ${s.function_type}`)}}for(let s of ca(tu,t.macros)){let c=o(s);switch(String(s.macro_type).toUpperCase()){case"SCALAR":c.scalar_macros.push(s);break;case"TABLE":c.table_macros.push(s);break;default:throw Error(`unknown catalog macro type ${s.macro_type}`)}}for(let s of ca(eu,t.indexes))o(s).indexes.push(s);return r}function Yn(t,e){if(!t)return[];return(Array.isArray(t)?t:[...t]).filter((r)=>r!=null).map((r)=>e(pe(r)))}function Pb(t){let e={};if(!t)return e;let n=t;if(typeof n[Symbol.iterator]!=="function")return e;for(let r of n)if(Array.isArray(r))e[String(r[0])]=String(r[1]);else if(r&&typeof r==="object"){let a=r;e[String(a.key??a[0]??"")]=String(a.value??a[1]??"")}return e}function ks(t){if(Symbol.asyncIterator in t)return t[Symbol.asyncIterator]();let e=t[Symbol.iterator]();return{async next(){return e.next()}}}function Vs(t){return String(t).toUpperCase()}function MT(t){let e=t.get(j_);return e!=null&&e!=="0"&&e.toLowerCase()!=="false"}async function*Bu(t,e,n){let r=t;if(typeof r.exchangeRaw!=="function")throw new kt(`tableInOutFunction(${e}): the transport's stream session has no exchangeRaw(), which continued output needs to read each reply's vgi.status`);let a=r.exchangeRaw.bind(t),o=!1,s=await a({batch:n,metadata:new Map});for(;;){if(s===null){if(o)throw new kt(`tableInOutFunction(${e}): the worker ended the stream while output was pending`);return}let c=new Map([...s.batch.metadata??[],...s.metadata]),l=c.get(cp)===$_;if(MT(c)&&(l||o))throw new kt(`tableInOutFunction(${e}): the worker answered not_modified on a continued reply (${o?"answering a continuation tick":"one saying HAVE_MORE_OUTPUT"}); not_modified must be an input's only reply`);let f=[...on(s.batch)];if(f.length>0)yield f;if(!l)return;o=!0,s=await a({batch:An(n.schema),metadata:new Map([[cp,H_]])})}}class Cu{rpc;defaultAttachOpaqueData;constructor(t,e){this.rpc=Rb(t),this.defaultAttachOpaqueData=e?.attachOpaqueData??null}static async fromIroh(t,e={},n,r){if(!/^(iroh|httpi):\/\//.test(t))throw TypeError("VgiClient.fromIroh requires an iroh:// or httpi:// endpoint");let a=r;if(!a){let o=await Promise.resolve().then(() => (qb(),{}));a=t.startsWith("iroh://")?void 0:void 0}if(!a)throw TypeError("no Iroh connector is available in this runtime");return new Cu(await a(t,e),n)}async _doBind(t,e,n,r,a,o,s,c=null,l){let f={function_name:t,arguments:n,function_type:e,input_schema:r,settings:a??null,secrets:o??null,attach_opaque_data:c??this.defaultAttachOpaqueData,transaction_opaque_data:s??null,resolved_secrets_provided:!1},p=ys(f),_=await this.rpc.call("bind",gs(p));if(!_)throw new kt("bind returned null");let g=Re(_),w=fp(g);if(l)l(w);return{request:f,response:w}}async _doInit(t,e,n){let r=n?.orderBy??null,a=n?.tablesample??null,o={bind_call:t,output_schema:e.output_schema,bind_opaque_data:e.opaque_data,split_tokens:null,row_limit:null,projection_ids:n?.projectionIds??null,pushdown_filters:n?.pushdownFilters??null,join_keys:n?.joinKeys??[],phase:n?.phase??null,finalize_state_id:null,order_by_column_name:r?.columnName??null,order_by_direction:r?.direction??null,order_by_null_order:r?.nullOrder??null,order_by_limit:r?.limit==null?null:BigInt(r.limit),tablesample_percentage:a?.percentage??null,tablesample_seed:a?.seed==null?null:BigInt(a.seed),execution_id:n?.executionId??null,init_opaque_data:null,substream_id:null},s=dp(o),c=await this.rpc.stream("init",gs(s)),l=c.header;if(!l)throw new kt("init did not return a header");let f=pp(l);return{session:c,initResponse:f}}async*tableFunction(t){let e=t.arguments??new Fi,{request:n,response:r}=await this._doBind(t.functionName,"table",e,null,t.settings??null,null,t.transactionOpaqueData??null,t.attachOpaqueData??null,t.onBind??null),{session:a}=await this._doInit(n,r,{projectionIds:t.projectionIds,pushdownFilters:t.pushdownFilters,orderBy:t.orderBy,tablesample:t.tablesample,joinKeys:t.joinKeys}),o=r.output_schema;try{for await(let s of a)if(s.length>0)yield ke(s,o)}finally{a.close()}}async*scalarFunctionRows(t){let e=ks(t.input),n=await e.next();if(n.done)throw new kt(`scalarFunction(${t.functionName}): input iterator yielded no batches; at least one batch is required to determine the input schema`);let r=n.value,a=r.schema,o=t.arguments??new Fi,{request:s,response:c}=await this._doBind(t.functionName,"scalar",o,a,t.settings??null,t.secrets??null,t.transactionOpaqueData??null,t.attachOpaqueData??null,t.onBind??null),{session:l}=await this._doInit(s,c);try{let f=await l.exchange([...on(r)]);if(f.length>0)yield f;for await(let p of{[Symbol.asyncIterator]:()=>e}){let _=await l.exchange([...on(p)]);if(_.length>0)yield _}}finally{l.close()}}async*scalarFunction(t){let e=ks(t.input),n=await e.next();if(n.done)throw new kt(`scalarFunction(${t.functionName}): input iterator yielded no batches; at least one batch is required to determine the input schema`);let r=n.value,a=r.schema,o=t.arguments??new Fi,{request:s,response:c}=await this._doBind(t.functionName,"scalar",o,a,t.settings??null,t.secrets??null,t.transactionOpaqueData??null,t.attachOpaqueData??null,t.onBind??null),{session:l}=await this._doInit(s,c),f=c.output_schema;try{let p=await l.exchange([...on(r)]);if(p.length>0)yield ke(p,f);for await(let _ of{[Symbol.asyncIterator]:()=>e}){let g=await l.exchange([...on(_)]);if(g.length>0)yield ke(g,f)}}finally{l.close()}}async*tableInOutFunctionRows(t){let e=ks(t.input),n=await e.next();if(n.done)throw new kt(`tableInOutFunction(${t.functionName}): input iterator yielded no batches; at least one batch is required to determine the input schema`);let r=n.value,a=r.schema,o=t.arguments??new Fi,{request:s,response:c}=await this._doBind(t.functionName,"table",o,a,t.settings??null,null,t.transactionOpaqueData??null,t.attachOpaqueData??null,t.onBind??null),{session:l,initResponse:f}=await this._doInit(s,c,{projectionIds:t.projectionIds,pushdownFilters:t.pushdownFilters,phase:"INPUT",orderBy:t.orderBy,tablesample:t.tablesample,joinKeys:t.joinKeys});try{yield*Bu(l,t.functionName,r);for await(let _ of{[Symbol.asyncIterator]:()=>e})yield*Bu(l,t.functionName,_)}finally{l.close()}if(t.hasFinalize===!1)return;let{session:p}=await this._doInit(s,c,{projectionIds:t.projectionIds,pushdownFilters:t.pushdownFilters,phase:"FINALIZE",executionId:f.execution_id,orderBy:t.orderBy,tablesample:t.tablesample,joinKeys:t.joinKeys});try{for await(let _ of p)if(_.length>0)yield _}finally{p.close()}}async*tableInOutFunction(t){let e=ks(t.input),n=await e.next();if(n.done)throw new kt(`tableInOutFunction(${t.functionName}): input iterator yielded no batches; at least one batch is required to determine the input schema`);let r=n.value,a=r.schema,o=t.arguments??new Fi,{request:s,response:c}=await this._doBind(t.functionName,"table",o,a,t.settings??null,null,t.transactionOpaqueData??null,t.attachOpaqueData??null,t.onBind??null),{session:l,initResponse:f}=await this._doInit(s,c,{projectionIds:t.projectionIds,pushdownFilters:t.pushdownFilters,phase:"INPUT",orderBy:t.orderBy,tablesample:t.tablesample,joinKeys:t.joinKeys}),p=c.output_schema;try{for await(let g of Bu(l,t.functionName,r))yield ke(g,p);for await(let g of{[Symbol.asyncIterator]:()=>e})for await(let w of Bu(l,t.functionName,g))yield ke(w,p)}finally{l.close()}if(t.hasFinalize===!1)return;let{session:_}=await this._doInit(s,c,{projectionIds:t.projectionIds,pushdownFilters:t.pushdownFilters,phase:"FINALIZE",executionId:f.execution_id,orderBy:t.orderBy,tablesample:t.tablesample,joinKeys:t.joinKeys});try{for await(let g of _)if(g.length>0)yield ke(g,p)}finally{_.close()}}async*tableFunctionRows(t){let e=t.arguments??new Fi,{request:n,response:r}=await this._doBind(t.functionName,"table",e,null,t.settings??null,null,t.transactionOpaqueData??null,t.attachOpaqueData??null,t.onBind??null),{session:a}=await this._doInit(n,r,{projectionIds:t.projectionIds,pushdownFilters:t.pushdownFilters,orderBy:t.orderBy,tablesample:t.tablesample,joinKeys:t.joinKeys});try{for await(let o of a)if(o.length>0)yield o}finally{a.close()}}async catalogsInfo(){let t=await this.rpc.call("catalog_catalogs",{});if(!t)return[];let e=Re(t);return Yn(e.items,mp)}async catalogs(){return(await this.catalogsInfo()).map((e)=>e.name)}async catalogAttach(t,e){if(e?.options!=null&&e?.optionsBytes!=null)throw new kt("catalogAttach: cannot specify both `options` and `optionsBytes`");let n;if(e?.optionsBytes!=null)n=e.optionsBytes.byteLength===0?null:e.optionsBytes;else n=gp(e?.options);let r=m([i("name",u(),!1),i("options",d(),!0),i("data_version_spec",u(),!0),i("implementation_version",u(),!0),i("client_capabilities",d(),!0)]),a=ue({name:[t],options:[n],data_version_spec:[e?.dataVersionSpec??null],implementation_version:[e?.implementationVersion??null],client_capabilities:[e?.clientCapabilities?_s(hg,e.clientCapabilities):null]},r),o=await this.rpc.call("catalog_attach",gs(a));if(!o)throw new kt("catalog_attach returned null");let s=Re(o);return{attach_opaque_data:pe(s.attach_opaque_data),supports_transactions:s.supports_transactions??!1,supports_time_travel:s.supports_time_travel??!1,catalog_version_frozen:s.catalog_version_frozen??!1,catalog_version:Number(s.catalog_version??0),attach_opaque_data_required:s.attach_opaque_data_required??!0,default_schema:s.default_schema??null,settings:s.settings?(Array.isArray(s.settings)?s.settings:[...s.settings]).map(pe):[],secret_types:s.secret_types?(Array.isArray(s.secret_types)?s.secret_types:[...s.secret_types]).map(pe):[],attach_catalogs:s.attach_catalogs?(Array.isArray(s.attach_catalogs)?s.attach_catalogs:[...s.attach_catalogs]).map(pe):[],comment:s.comment??null,tags:Pb(s.tags),supports_column_statistics:s.supports_column_statistics??!1,global_functions:s.global_functions?(Array.isArray(s.global_functions)?s.global_functions:[...s.global_functions]).map(pe):[],global_function_prefix:s.global_function_prefix??"",resolved_data_version:s.resolved_data_version??null,resolved_implementation_version:s.resolved_implementation_version??null,supports_catalog_contents:s.supports_catalog_contents??!1}}async catalogContents(t,e=null){let n=await this.rpc.call("catalog_contents",{attach_opaque_data:t,if_none_match:e});if(!n)throw new kt("catalog_contents returned null");let r=bg(pe(n.result)),a=Lb(r,e);return{catalog_version:Number(r.catalog_version??0),etag:r.etag??null,not_modified:Boolean(r.not_modified),schemas:a}}async loadCatalog(t,e){let n=t.attach_opaque_data,r=e?.previous??null;if(!(t.supports_catalog_contents===!0&&e?.useCatalogContents!==!1&&e?.transactionOpaqueData==null))return this._loadCatalogPerSchema(n,e?.transactionOpaqueData,null);let o=null,s=r?.etag??null,c=r?.catalog_version??0;for(let l=0;l<2;l++){let f;try{f=await this.catalogContents(n,s)}catch(p){o=p instanceof Error?p.message:String(p);break}if(f.not_modified){if(r==null||s==null){o="catalog_contents answered not_modified to an unconditional request";break}return{...r,catalog_version:f.catalog_version!==0?f.catalog_version:r.catalog_version,etag:f.etag??s,not_modified:!0,fallback_error:null}}if(f.catalog_version!==0&&c!==0&&f.catalog_version{let _=s[p];return _==null||Number(_)!==0},f=e;a.push({schema:o,tables:l("table")?await this.schemaContentsTables(t,c,f):[],views:l("view")?await this.schemaContentsViews(t,c,f):[],scalar_functions:l("scalar_function")?await this.schemaContentsFunctions(t,c,"SCALAR_FUNCTION",f):[],aggregate_functions:l("aggregate_function")?await this.schemaContentsFunctions(t,c,"AGGREGATE_FUNCTION",f):[],table_functions:l("table_function")?await this.schemaContentsFunctions(t,c,"TABLE_FUNCTION",f):[],scalar_macros:l("macro")?await this.schemaContentsMacros(t,c,"SCALAR_MACRO",f):[],table_macros:l("macro")?await this.schemaContentsMacros(t,c,"TABLE_MACRO",f):[],indexes:l("index")?await this.schemaContentsIndexes(t,c,f):[]})}return{catalog_version:null,etag:null,source:"per_schema",not_modified:!1,fallback_error:n,schemas:a}}async catalogDetach(t){await this.rpc.call("catalog_detach",{attach_opaque_data:t})}async catalogCreate(t,e,n){let r;if(n==null)r=null;else if(n instanceof Uint8Array)r=n.byteLength===0?null:n;else{if(n.options!=null&&n.optionsBytes!=null)throw new kt("catalogCreate: cannot specify both `options` and `optionsBytes`");if(n.optionsBytes!=null)r=n.optionsBytes.byteLength===0?null:n.optionsBytes;else r=gp(n.options)}await this.rpc.call("catalog_create",{name:t,on_conflict:Vs(e),options:r})}async catalogDrop(t){await this.rpc.call("catalog_drop",{name:t})}async catalogVersion(t,e){let n=await this.rpc.call("catalog_version",{attach_opaque_data:t,transaction_opaque_data:e??null});if(!n)throw new kt("catalog_version returned null");let r=Re(n);return Number(r.version??0)}async transactionBegin(t){let e=await this.rpc.call("catalog_transaction_begin",{attach_opaque_data:t});if(!e)throw new kt("transaction_begin returned null");let n=Re(e);if(!n.transaction_opaque_data)throw new kt("transaction_begin returned no transaction_opaque_data");return pe(n.transaction_opaque_data)}async transactionCommit(t,e){await this.rpc.call("catalog_transaction_commit",{attach_opaque_data:t,transaction_opaque_data:e})}async transactionRollback(t,e){await this.rpc.call("catalog_transaction_rollback",{attach_opaque_data:t,transaction_opaque_data:e})}async schemas(t,e){let n=await this.rpc.call("catalog_schemas",{attach_opaque_data:t,transaction_opaque_data:e??null});if(!n)return[];let r=Re(n);return Yn(r.items,iu)}async schemaGet(t,e,n){let r=await this.rpc.call("catalog_schema_get",{attach_opaque_data:t,path:St(e),transaction_opaque_data:n??null});if(!r)return null;let a=Re(r),o=Yn(a.items,iu);return o.length>0?o[0]:null}async schemaCreate(t,e,n){let r=n?.tags?new Map(Object.entries(n.tags)):null;await this.rpc.call("catalog_schema_create",{attach_opaque_data:t,path:St(e),on_conflict:Vs(n?.onConflict??"error"),comment:n?.comment??null,tags:r,transaction_opaque_data:n?.transactionOpaqueData??null})}async schemaDrop(t,e,n,r,a){await this.rpc.call("catalog_schema_drop",{attach_opaque_data:t,path:St(e),ignore_not_found:n??!1,cascade:r??!1,transaction_opaque_data:a??null})}async schemaContentsTables(t,e,n){let r=await this.rpc.call("catalog_schema_contents_tables",{attach_opaque_data:t,path:St(e),transaction_opaque_data:n??null});if(!r)return[];let a=Re(r);return Yn(a.items,au)}async schemaContentsViews(t,e,n){let r=await this.rpc.call("catalog_schema_contents_views",{attach_opaque_data:t,path:St(e),transaction_opaque_data:n??null});if(!r)return[];let a=Re(r);return Yn(a.items,ou)}async schemaContentsFunctions(t,e,n,r){let a=await this.rpc.call("catalog_schema_contents_functions",{attach_opaque_data:t,path:St(e),type:n,transaction_opaque_data:r??null});if(!a)return[];let o=Re(a);return Yn(o.items,yp)}async schemaContentsIndexes(t,e,n){let r=await this.rpc.call("catalog_schema_contents_indexes",{attach_opaque_data:t,path:St(e),transaction_opaque_data:n??null});if(!r)return[];let a=Re(r);return Yn(a.items,_p)}async tableGet(t,e,n,r){let a=await this.rpc.call("catalog_table_get",{attach_opaque_data:t,schema_path:St(e),name:n,transaction_opaque_data:r??null});if(!a)return null;let o=Re(a),s=Yn(o.items,au);return s.length>0?s[0]:null}async tableCreate(t,e,n,r,a,o=[],s){await this.rpc.call("catalog_table_create",{request:_s(yg,{attach_opaque_data:t,schema_path:St(e),name:n,columns:r,on_conflict:Vs(a),constraint_definitions:o,transaction_opaque_data:s??null})})}async tableDrop(t,e,n,r,a,o){await this.rpc.call("catalog_table_drop",{attach_opaque_data:t,schema_path:St(e),name:n,ignore_not_found:r??!1,cascade:a??!1,transaction_opaque_data:o??null})}async tableScanFunctionGet(t,e,n,r,a,o){let s=await this.rpc.call("catalog_table_scan_function_get",{attach_opaque_data:t,schema_path:St(e),name:n,at_unit:r??null,at_value:a??null,transaction_opaque_data:o??null});if(!s)throw new kt("table_scan_function_get returned null");return wg(Re(s))}async tableCommentSet(t,e,n,r,a,o){await this.rpc.call("catalog_table_comment_set",{attach_opaque_data:t,schema_path:St(e),name:n,comment:r??null,ignore_not_found:a??null,transaction_opaque_data:o??null})}async tableRename(t,e,n,r,a,o){await this.rpc.call("catalog_table_rename",{attach_opaque_data:t,schema_path:St(e),name:n,new_name:r,ignore_not_found:a??null,transaction_opaque_data:o??null})}async tableColumnAdd(t,e,n,r,a,o=!1,s=!1,c){await this.rpc.call("catalog_table_column_add",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,column_definition:a,ignore_not_found:o,if_column_not_exists:s,transaction_opaque_data:c??null})}async tableColumnDrop(t,e,n,r,a=!1,o=!1,s=!1,c){await this.rpc.call("catalog_table_column_drop",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,ignore_not_found:a,if_column_exists:o,cascade:s,transaction_opaque_data:c??null})}async tableColumnRename(t,e,n,r,a,o=!1,s){await this.rpc.call("catalog_table_column_rename",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,new_column_name:a,ignore_not_found:o,transaction_opaque_data:s??null})}async tableColumnCommentSet(t,e,n,r,a,o=!1,s){await this.rpc.call("catalog_table_column_comment_set",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,comment:a,ignore_not_found:o,transaction_opaque_data:s??null})}async tableColumnDefaultSet(t,e,n,r,a,o=!1,s){await this.rpc.call("catalog_table_column_default_set",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,expression:a,ignore_not_found:o,transaction_opaque_data:s??null})}async tableColumnDefaultDrop(t,e,n,r,a=!1,o){await this.rpc.call("catalog_table_column_default_drop",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,ignore_not_found:a,transaction_opaque_data:o??null})}async tableColumnTypeChange(t,e,n,r,a,o=null,s=!1,c){await this.rpc.call("catalog_table_column_type_change",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,column_definition:a,expression:o,ignore_not_found:s,transaction_opaque_data:c??null})}async tableNotNullSet(t,e,n,r,a=!1,o){await this.rpc.call("catalog_table_not_null_set",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,ignore_not_found:a,transaction_opaque_data:o??null})}async tableNotNullDrop(t,e,n,r,a=!1,o){await this.rpc.call("catalog_table_not_null_drop",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,ignore_not_found:a,transaction_opaque_data:o??null})}async viewGet(t,e,n,r){let a=await this.rpc.call("catalog_view_get",{attach_opaque_data:t,schema_path:St(e),name:n,transaction_opaque_data:r??null});if(!a)return null;let o=Re(a),s=Yn(o.items,ou);return s.length>0?s[0]:null}async viewCreate(t,e,n,r,a,o,s,c="regular",l,f,p){await this.rpc.call("catalog_view_create",{attach_opaque_data:t,schema_path:St(e),name:n,definition:r,aliases:s??null,security:c,comment:l??null,tags:f??null,column_comments:p??null,on_conflict:Vs(a),transaction_opaque_data:o??null})}async viewDrop(t,e,n,r,a,o){await this.rpc.call("catalog_view_drop",{attach_opaque_data:t,schema_path:St(e),name:n,ignore_not_found:r??!1,cascade:a??!1,transaction_opaque_data:o??null})}async viewRename(t,e,n,r,a,o){await this.rpc.call("catalog_view_rename",{attach_opaque_data:t,schema_path:St(e),name:n,new_name:r,ignore_not_found:a??null,transaction_opaque_data:o??null})}async viewCommentSet(t,e,n,r,a,o){await this.rpc.call("catalog_view_comment_set",{attach_opaque_data:t,schema_path:St(e),name:n,comment:r??null,ignore_not_found:a??null,transaction_opaque_data:o??null})}async viewColumnCommentSet(t,e,n,r,a,o=!1,s){await this.rpc.call("catalog_view_column_comment_set",{attach_opaque_data:t,schema_path:St(e),name:n,column_path:r,comment:a??null,ignore_not_found:o,transaction_opaque_data:s??null})}async macroGet(t,e,n,r){let a=await this.rpc.call("catalog_macro_get",{attach_opaque_data:t,schema_path:St(e),name:n,transaction_opaque_data:r??null});if(!a)return null;let o=Re(a),s=Yn(o.items,ru);return s.length>0?s[0]:null}async schemaContentsMacros(t,e,n,r){let a=await this.rpc.call("catalog_schema_contents_macros",{attach_opaque_data:t,path:St(e),type:n,transaction_opaque_data:r??null});if(!a)return[];let o=Re(a);return Yn(o.items,ru)}async macroCreate(t,e,n,r,a,o,s){let c=ue({attach_opaque_data:[t],schema_path:[St(e)],name:[n],macro_type:[r],overloads:[a],on_conflict:[Vs(o)],transaction_opaque_data:[s??null]},mg);await this.rpc.call("catalog_macro_create",gs(c))}async macroDrop(t,e,n,r,a){await this.rpc.call("catalog_macro_drop",{attach_opaque_data:t,schema_path:St(e),name:n,ignore_not_found:r??!1,transaction_opaque_data:a??null})}close(){this.rpc.close()}}function zb(t){let e=[],n=0;for(let r of t.fields){let a=r.metadata,s=a.get(W_)===Q_?r.name:n++,c=a.get(X_),l=c===J_,f=c===Z_,p=a.get(tg)===eg,_=a.get(ng)===rg,g=a.get(ig)??"",w=a.get(ag),I=a.get(og),x=a.get(sg),C=a.get(cg);e.push({name:r.name,position:s,arrowType:r.type,isTableInput:l,isAnyType:f,isVarargs:p,isConst:_,doc:g,defaultJson:w,choicesJson:I,rangeNotation:x,pattern:C})}return e}var w3=m([i("name",u(),!1),i("description",u(),!1),i("type",d(),!1),i("default_value",d(),!0),i("required",v(),!0),i("secret",v(),!0)]);function jb(t){let e=[...on(fe(t))][0];if(e===void 0)throw Error("Cannot deserialize AttachOptionSpec from an empty batch");let r=We(pe(e.type)).fields[0].type,a,o=e.default_value;if(o!=null){let s=pe(o);if(s.length>0)a=[...on(fe(s))][0]?.value}return{name:String(e.name),description:String(e.description??""),type:r,...a===void 0||a===null?{}:{default:a},required:e.required===!0,secret:e.secret===!0}}function $b(t){return Array.from(t,jb)}Gp();export{lp as FunctionType,X as TypeId,Cu as VgiClient,kt as VgiClientError,$b as deserializeAttachOptionSpecs,We as deserializeSchema,TT as httpConnect,zb as schemaToArgumentSpecs}; diff --git a/vgi/invocation.py b/vgi/invocation.py index 1d8a76a3..d1831c12 100644 --- a/vgi/invocation.py +++ b/vgi/invocation.py @@ -15,13 +15,14 @@ from __future__ import annotations +import re import uuid from dataclasses import dataclass, field from enum import Enum -from typing import Annotated +from typing import Annotated, Self import pyarrow as pa -from vgi_rpc import ArrowSerializableDataclass, ArrowType +from vgi_rpc import ArrowSerializableDataclass, ArrowType, IpcValidation from vgi.arguments import SecretLookupEntry from vgi.metadata import DEFAULT_MAX_WORKERS @@ -30,6 +31,7 @@ "FunctionType", "BaseInitResponse", "BindResponse", + "BindRepeatability", "GlobalInitResponse", ] @@ -48,6 +50,20 @@ class FunctionType(Enum): TABLE_BUFFERING = "table_buffering" +class BindRepeatability(Enum): + """Repeatability of equivalent scans derived from one complete bind state. + + ``STABLE_WITHIN_QUERY`` promises the same logical rows and values during a + single query. It does not combine independently bound sources into one + snapshot, promise row order or batch boundaries, authorize retries, or + establish cross-query cache validity. A provider anchors the promise in + existing opaque bind state or an existing transaction snapshot. + """ + + UNKNOWN = "unknown" + STABLE_WITHIN_QUERY = "stable_within_query" + + @dataclass(frozen=True, slots=True, kw_only=True) class BindResponse(ArrowSerializableDataclass): """The result of calling bind() on a function. @@ -70,6 +86,10 @@ class BindResponse(ArrowSerializableDataclass): lookup_secret_types: Secret types for scoped lookup requests (empty = normal response). lookup_scopes: Scopes for scoped lookup requests (parallel to lookup_secret_types). lookup_names: Names for scoped lookup requests (parallel to lookup_secret_types). + projection_expression_profiles: Scalar expression profiles offered for optional + projection proposals on this bind; an empty list offers none. + repeatability: Optional provider promise for equivalent scans of this + complete bind state within one query; unknown makes no promise. """ @@ -85,6 +105,46 @@ class BindResponse(ArrowSerializableDataclass): lookup_secret_types: list[str] = field(default_factory=list) lookup_scopes: list[str] = field(default_factory=list) lookup_names: list[str] = field(default_factory=list) + projection_expression_profiles: list[str] = field(default_factory=list) + repeatability: BindRepeatability = BindRepeatability.UNKNOWN + + def __post_init__(self) -> None: + """Validate the per-bind offer without granting projection acceptance.""" + if not isinstance(self.repeatability, BindRepeatability): + raise ValueError("invalid bind repeatability") + profiles = self.projection_expression_profiles + if ( + not isinstance(profiles, list) + or len(profiles) > 64 + or any( + not isinstance(profile, str) + or len(profile) > 256 + or re.fullmatch(r"[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*\.v[1-9][0-9]*", profile) is None + for profile in profiles + ) + or len(set(profiles)) != len(profiles) + ): + raise ValueError("invalid projection expression profile offer") + + @classmethod + def deserialize_from_batch( + cls, + batch: pa.RecordBatch, + custom_metadata: pa.KeyValueMetadata | None = None, + *, + ipc_validation: IpcValidation = IpcValidation.FULL, + ) -> Self: + """Require the successor promise before generic legacy enum decoding.""" + index = batch.schema.get_field_index("repeatability") + if batch.num_rows != 1 or index < 0: + raise ValueError("bind repeatability must be present in one row") + field = batch.schema.field(index) + if field.nullable or field.type != pa.dictionary(pa.int16(), pa.string()): + raise ValueError("bind repeatability requires nonnullable dictionary") + value = batch.column(index)[0].as_py() + if value not in BindRepeatability.__members__: + raise ValueError("invalid bind repeatability wire value") + return super(BindResponse, cls).deserialize_from_batch(batch, custom_metadata, ipc_validation=ipc_validation) @property def is_secret_scope_request(self) -> bool: diff --git a/vgi/logical_types.py b/vgi/logical_types.py new file mode 100644 index 00000000..9028e285 --- /dev/null +++ b/vgi/logical_types.py @@ -0,0 +1,267 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""TUPLE identity and the canonical Arrow Parquet Variant carrier. + +VARIANT bytes follow Apache Parquet's VariantEncoding, never a VGI encoding. +These helpers validate the Arrow carrier only. The consuming backend owns binary +Variant decoding and semantic support; see docs/protocol/logical-types.md. +""" + +from __future__ import annotations + +from collections.abc import Collection, Mapping, Sequence +from typing import Any, cast + +import pyarrow as pa + +from vgi.typed_values import TypedValueBatch + +LOGICAL_TYPE_KEY = b"vgi.logical_type.v1" +EXTENSION_NAME_KEY = b"ARROW:extension:name" +EXTENSION_METADATA_KEY = b"ARROW:extension:metadata" +VARIANT_EXTENSION_NAME = b"arrow.parquet.variant" +TUPLE_PROFILE = "vgi.type.tuple.v1" +VARIANT_PROFILE = "vgi.type.variant.parquet.v1" +LOGICAL_TYPE_PROFILES = (TUPLE_PROFILE, VARIANT_PROFILE) +MAX_VARIANT_BYTES = 8 * 1024 * 1024 +MAX_LOGICAL_DEPTH = 64 +MAX_LOGICAL_FIELDS = 4096 + + +def tuple_field( + name: str, + children: Sequence[pa.Field[Any]], + *, + nullable: bool = True, + metadata: Mapping[bytes, bytes] | None = None, +) -> pa.Field[Any]: + """Declare an unnamed tuple, retaining all recursive child metadata.""" + values = dict(metadata or {}) + if LOGICAL_TYPE_KEY in values and values[LOGICAL_TYPE_KEY] != b"tuple": + raise ValueError("conflicting logical type metadata") + values[LOGICAL_TYPE_KEY] = b"tuple" + fields = [field.with_name(f"element{i + 1}") for i, field in enumerate(children)] + return pa.field(name, pa.struct(fields), nullable=nullable, metadata=values) + + +def variant_field(name: str, *, nullable: bool = True, metadata: Mapping[bytes, bytes] | None = None) -> pa.Field[Any]: + """Declare canonical unshredded Arrow Parquet Variant storage.""" + values = dict(metadata or {}) + if any(key.startswith(b"vgi.logical_type.") for key in values): + raise ValueError("Parquet Variant must use the canonical Arrow extension annotation") + if values.get(EXTENSION_NAME_KEY, VARIANT_EXTENSION_NAME) != VARIANT_EXTENSION_NAME: + raise ValueError("conflicting Arrow extension name") + if values.get(EXTENSION_METADATA_KEY, b"") != b"": + raise ValueError("Parquet Variant extension metadata must be empty") + values[EXTENSION_NAME_KEY] = VARIANT_EXTENSION_NAME + values[EXTENSION_METADATA_KEY] = b"" + storage = pa.struct([pa.field("metadata", pa.binary(), nullable=False), pa.field("value", pa.binary())]) + return pa.field(name, storage, nullable=nullable, metadata=values) + + +def select_logical_type_profiles(offered: Collection[str], supported: Collection[str]) -> list[str]: + """Select known common profiles; an empty offer never selects a profile.""" + return sorted(set(offered) & set(supported) & set(LOGICAL_TYPE_PROFILES)) + + +def _metadata(field: pa.Field[Any]) -> dict[bytes, bytes]: + if not field.equals(field.with_metadata(cast(Any, field.metadata)), check_metadata=True): + raise ValueError("logical type field metadata keys must be unique") + return field.metadata or {} + + +def _registered_variant(dtype: pa.DataType) -> bool: + return isinstance(dtype, pa.BaseExtensionType) and dtype.extension_name == VARIANT_EXTENSION_NAME.decode() + + +def _is_variant(field: pa.Field[Any]) -> bool: + return (field.metadata or {}).get(EXTENSION_NAME_KEY) == VARIANT_EXTENSION_NAME or _registered_variant(field.type) + + +def _storage(dtype: pa.DataType) -> pa.DataType: + return dtype.storage_type if isinstance(dtype, pa.BaseExtensionType) else dtype + + +def _binary_type(dtype: pa.DataType, *, allow_encoded: bool = False) -> bool: + if allow_encoded and (pa.types.is_dictionary(dtype) or pa.types.is_run_end_encoded(dtype)): + return _binary_type(dtype.value_type, allow_encoded=True) + return pa.types.is_binary(dtype) or pa.types.is_large_binary(dtype) or pa.types.is_binary_view(dtype) + + +def _check_variant(field: pa.Field[Any], selected: Collection[str]) -> None: + if VARIANT_PROFILE not in selected: + raise ValueError("variant logical type profile was not negotiated") + metadata = _metadata(field) + if metadata.get(EXTENSION_METADATA_KEY, b"") != b"": + raise ValueError("Parquet Variant extension metadata must be empty") + if isinstance(field.type, pa.BaseExtensionType): + if not _registered_variant(field.type): + raise ValueError("conflicting registered Arrow extension type") + if cast(Any, field.type).__arrow_ext_serialize__() != b"": + raise ValueError("Parquet Variant extension metadata must be empty") + if metadata.get(EXTENSION_NAME_KEY, VARIANT_EXTENSION_NAME) != VARIANT_EXTENSION_NAME: + raise ValueError("conflicting Arrow extension name") + dtype = _storage(field.type) + if not pa.types.is_struct(dtype): + raise ValueError("Parquet Variant requires struct storage") + names = [child.name for child in dtype] + if "typed_value" in names: + raise ValueError("shredded Parquet Variant is not supported by this profile") + if sorted(names) != ["metadata", "value"]: + raise ValueError("unshredded Parquet Variant requires exactly metadata and value fields") + meta_field = dtype.field("metadata") + value_field = dtype.field("value") + if meta_field.nullable or not _binary_type(meta_field.type, allow_encoded=True): + raise ValueError("Parquet Variant metadata must be non-nullable binary storage") + if not _binary_type(value_field.type): + raise ValueError("Parquet Variant value must use binary storage") + + +def _check_field(field: pa.Field[Any], selected: Collection[str], depth: int, budget: list[int]) -> None: + if depth > MAX_LOGICAL_DEPTH: + raise ValueError("logical type nesting is too deep") + budget[0] += 1 + if budget[0] > MAX_LOGICAL_FIELDS: + raise ValueError("too many logical type fields") + metadata = _metadata(field) + if any(key.startswith(b"vgi.logical_type.") and key != LOGICAL_TYPE_KEY for key in metadata): + raise ValueError("unsupported logical type annotation version") + kind = metadata.get(LOGICAL_TYPE_KEY) + if kind is not None: + if kind != b"tuple": + raise ValueError("unsupported logical type annotation; VARIANT uses arrow.parquet.variant") + if TUPLE_PROFILE not in selected: + raise ValueError("tuple logical type profile was not negotiated") + if EXTENSION_NAME_KEY in metadata or isinstance(field.type, pa.BaseExtensionType): + raise ValueError("tuple cannot also declare an Arrow extension") + if not pa.types.is_struct(field.type): + raise ValueError("tuple requires an Arrow struct") + if [child.name for child in field.type] != [f"element{i + 1}" for i in range(field.type.num_fields)]: + raise ValueError("tuple members require positional element1 through elementN names") + if _is_variant(field): + _check_variant(field, selected) + dtype = _storage(field.type) + for i in range(dtype.num_fields): + _check_field(dtype.field(i), selected, depth + 1, budget) + if pa.types.is_dictionary(dtype): + _check_field(pa.field("dictionary-value", dtype.value_type), selected, depth + 1, budget) + + +def validate_logical_schema(schema: pa.Schema, *, selected_profiles: Collection[str] = ()) -> None: + """Validate recursive annotations without restricting ordinary Arrow types.""" + if not schema.equals(schema.with_metadata(schema.metadata), check_metadata=True): + raise ValueError("logical type schema metadata keys must be unique") + budget = [0] + for field in schema: + _check_field(field, selected_profiles, 0, budget) + + +def _contains_annotation(field: pa.Field[Any], depth: int = 0) -> bool: + if depth > MAX_LOGICAL_DEPTH: + raise ValueError("logical type nesting is too deep") + if any(key.startswith(b"vgi.logical_type.") for key in (field.metadata or {})) or _is_variant(field): + return True + dtype = _storage(field.type) + if any(_contains_annotation(dtype.field(i), depth + 1) for i in range(dtype.num_fields)): + return True + if pa.types.is_dictionary(dtype): + return _contains_annotation(pa.field("dictionary-value", dtype.value_type), depth + 1) + return False + + +def require_logical_profiles(schema: pa.Schema, *, selected_profiles: Collection[str] = ()) -> bool: + """Check annotated subtrees; leave legacy unannotated schemas unchanged.""" + annotated = False + for field in schema: + if _contains_annotation(field): + _check_field(field, selected_profiles, 0, [0]) + annotated = True + return annotated + + +def _decoded_scalar(value: pa.Scalar[Any] | None) -> pa.Scalar[Any] | None: + while isinstance(value, (pa.DictionaryScalar, pa.ExtensionScalar, pa.RunEndEncodedScalar)): + if not value.is_valid: + return None + value = cast("pa.Scalar[Any] | None", value.value) + return value + + +def _variant_carrier(value: pa.Scalar[Any]) -> None: + scalar = _decoded_scalar(value) + if scalar is None or not scalar.is_valid: + return + if not isinstance(scalar, pa.StructScalar): + raise ValueError("Parquet Variant requires a struct scalar") + total = 0 + for name in ("metadata", "value"): + child = _decoded_scalar(cast(Any, scalar)[name]) + if child is None or not child.is_valid: + if name == "metadata" or not scalar.type.field(name).nullable: + raise ValueError(f"present Parquet Variant requires non-NULL {name}") + continue + total += cast(Any, child).as_buffer().size + if total > MAX_VARIANT_BYTES: + raise ValueError("Parquet Variant carrier exceeds its byte budget") + # Deliberately no binary parser here: a backend must validate and decode + # Apache Parquet VariantEncoding before interpreting these opaque bytes. + + +def _require_annotated_value( + value: pa.Scalar[Any] | None, field: pa.Field[Any], profiles: Collection[str], depth: int = 0 +) -> None: + if depth > MAX_LOGICAL_DEPTH: + raise ValueError("logical value nesting is too deep") + if not _contains_annotation(field): + return + if value is None or not value.is_valid: + if not field.nullable: + raise ValueError("NULL supplied for a non-nullable logical field") + return + if _is_variant(field): + _variant_carrier(value) + return + if isinstance(value, pa.StructScalar): + for i, child in enumerate(value.type): + _require_annotated_value(cast(Any, value)[i], child, profiles, depth + 1) + elif isinstance(value, pa.MapScalar): + assert value.values is not None + for pair in value.values: + _require_annotated_value(pair[0], value.type.key_field, profiles, depth + 1) + _require_annotated_value(pair[1], value.type.item_field, profiles, depth + 1) + elif isinstance( + value, (pa.ListScalar, pa.LargeListScalar, pa.FixedSizeListScalar, pa.ListViewScalar, pa.LargeListViewScalar) + ): + assert value.values is not None + for item in value.values: + _require_annotated_value(item, value.type.value_field, profiles, depth + 1) + elif isinstance(value, pa.UnionScalar): + child = value.type[value.type.type_codes.index(cast(int, value.type_code))] + _require_annotated_value(value.value, child, profiles, depth + 1) + elif isinstance(value, (pa.DictionaryScalar, pa.ExtensionScalar, pa.RunEndEncodedScalar)): + decoded = cast("pa.Scalar[Any] | None", value.value) + if decoded is not None: + _require_annotated_value(decoded, field.with_type(decoded.type), profiles, depth + 1) + + +def require_logical_batch(batch: pa.RecordBatch, *, selected_profiles: Collection[str] = ()) -> None: + """Check every annotated output carrier; skip ordinary unannotated columns.""" + if not require_logical_profiles(batch.schema, selected_profiles=selected_profiles): + return + batch.validate(full=True) + for i, field in enumerate(batch.schema): + if _contains_annotation(field): + for scalar in batch.column(i): + _require_annotated_value(scalar, field, selected_profiles) + + +def require_logical_values(values: TypedValueBatch, *, selected_profiles: Collection[str] = ()) -> None: + """Check negotiated logical annotations and canonical Variant carriers.""" + require_logical_batch(values.batch, selected_profiles=selected_profiles) + + +def validate_logical_values(values: TypedValueBatch, *, selected_profiles: Collection[str] = ()) -> None: + """Validate typed values and carrier shape without decoding Variant bytes.""" + values.__post_init__() + validate_logical_schema(values.schema, selected_profiles=selected_profiles) + require_logical_batch(values.batch, selected_profiles=selected_profiles) diff --git a/vgi/meta_worker.py b/vgi/meta_worker.py index eb341a7a..bf3dec4a 100644 --- a/vgi/meta_worker.py +++ b/vgi/meta_worker.py @@ -38,6 +38,8 @@ from vgi._redact import short_hash from vgi.catalog.catalog_interface import CatalogAttachResult +from vgi.catalog.constraints import ConstraintsApplyRequest, ConstraintSnapshot +from vgi.catalog.query import CatalogQueryPrepareRequest, CatalogQueryPrepareResult from vgi.exceptions import NotFoundError from vgi.invocation import GlobalInitResponse from vgi.protocol import ( @@ -106,6 +108,11 @@ def method(self: MetaWorker, **kwargs: Any) -> Any: "catalog_contents", "catalog_schema_get", "catalog_schema_create", + "catalog_schema_option_definitions", + "catalog_schema_alter_options", + "catalog_schema_alter_metadata", + "catalog_dependencies", + "catalog_query_release", "catalog_schema_drop", "catalog_schema_contents_tables", "catalog_schema_contents_views", @@ -113,6 +120,9 @@ def method(self: MetaWorker, **kwargs: Any) -> Any: "catalog_schema_contents_macros", "catalog_copy_from_formats", "catalog_table_get", + "catalog_table_option_definitions", + "catalog_table_alter_options", + "catalog_table_alter_layout", "catalog_table_drop", "catalog_table_scan_function_get", "catalog_table_scan_branches_get", @@ -136,9 +146,11 @@ def method(self: MetaWorker, **kwargs: Any) -> Any: "catalog_view_drop", "catalog_view_rename", "catalog_view_comment_set", + "catalog_view_column_comment_set", "catalog_macro_get", "catalog_macro_drop", "catalog_index_get", + "catalog_index_option_definitions", "catalog_index_drop", "catalog_schema_contents_indexes", ] @@ -325,6 +337,14 @@ def _unwrap_attach_full(self, envelope: bytes | None) -> bytes | None: worker = self._maybe_worker_for_attach(envelope) or self._workers[0] return worker._unwrap_attach_full(envelope) + def _unwrap_transaction_for(self, request: Any) -> bytes | None: + """Re-open a function transaction on its owning worker during continuation.""" + bind = getattr(request, "bind_call", request) + if not getattr(bind, "transaction_opaque_data", None): + return None + worker = self._maybe_worker_for_attach(getattr(bind, "attach_opaque_data", None)) or self._workers[0] + return worker._unwrap_transaction_for(request) + def _resolve_function(self, request: BindRequest) -> Any: """Dispatch function-class resolution to the worker that hosts it. @@ -440,6 +460,12 @@ def catalog_table_create(self, request: Any) -> None: request ) + def catalog_table_constraints_apply(self, request: ConstraintsApplyRequest) -> ConstraintSnapshot: + """Route by the sealed attachment; the selected child owns unwrapping.""" + return self._worker_for_attach( + request.attach_opaque_data, method_name="catalog_table_constraints_apply" + ).catalog_table_constraints_apply(request) + def catalog_macro_create(self, request: Any) -> None: """Create a macro — dispatch via attach_opaque_data in request.""" self._worker_for_attach(request.attach_opaque_data, method_name="catalog_macro_create").catalog_macro_create( @@ -542,6 +568,17 @@ def table_function_cardinality(self, request: Any, ctx: CallContext) -> Any: msg = f"Unknown function '{fn_name}'" raise NotFoundError(msg) + def table_function_projection(self, request: Any, ctx: CallContext) -> Any: + """Route an optional pure projection proposal to the source's owner.""" + patched, worker = self._unwrap_bind_call_attach_opaque_data(request, method_name="table_function_projection") + if worker is not None: + return worker.table_function_projection(patched, ctx=ctx) + fn_name = request.bind_call.function_name + for candidate in self._workers: + if fn_name in type(candidate)._build_registry(): + return candidate.table_function_projection(request, ctx=ctx) + raise ValueError(f"Unknown function '{fn_name}'") + def table_function_plan(self, request: Any, ctx: CallContext) -> Any: """Dispatch scan planning to the right worker.""" patched, worker = self._unwrap_bind_call_attach_opaque_data( @@ -723,6 +760,12 @@ def _load_table_buffering_params( ) raise NotFoundError(f"Unknown table_buffering function '{fn_name}'") + def catalog_query_prepare(self, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + """Route preparation through the same authenticated attachment as execution.""" + return self._worker_for_attach( + request.attach_opaque_data, method_name="catalog_query_prepare" + ).catalog_query_prepare(request) + def hosted_protocols(self) -> tuple[tuple[type, object], ...]: """Return every child worker class's ``hosted_protocols()``, in order. diff --git a/vgi/protocol.py b/vgi/protocol.py index b29233fa..cb82048c 100644 --- a/vgi/protocol.py +++ b/vgi/protocol.py @@ -47,12 +47,15 @@ from vgi.arguments import Arguments from vgi.cache_control import CACHE_SCOPE_CATALOG, CacheControl +from vgi.catalog.capabilities import ( + ROW_SCALAR_EXPRESSION, + CatalogCapabilities, +) from vgi.catalog.catalog_interface import ( CatalogAttachResult, CatalogInfo, CopyFromFormatInfo, FunctionInfo, - IndexConstraintType, IndexInfo, MacroInfo, MacroType, @@ -63,6 +66,16 @@ TableInfo, ViewInfo, ) +from vgi.catalog.column_metadata import validate_column_metadata +from vgi.catalog.constraints import ConstraintsApplyRequest, ConstraintSnapshot, constraint_definitions +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot +from vgi.catalog.evaluation_context import _SqlRecord +from vgi.catalog.indexes import IndexDefinition +from vgi.catalog.layout import TableLayout +from vgi.catalog.macros import MAX_MACRO_BYTES, MacroOverload, validate_macro_family +from vgi.catalog.options import OptionDefinitions +from vgi.catalog.query import CatalogQueryPrepareRequest, CatalogQueryPrepareResult, ClientQueryCapabilities +from vgi.catalog.sql import SqlExpression from vgi.filter_v2 import delta_revisions, replaying_accepted_state from vgi.function import StreamStateCodec, _is_state_codec from vgi.function_storage import BoundStorage, FrameworkNS, attach_catalog_bytes @@ -84,9 +97,12 @@ project_schema, ) from vgi.table_in_out_function import TableInOutGenerator +from vgi.typed_values import TypedValueBatch __all__ = [ "BindRequest", + "TableFunctionProjectionRequest", + "TableFunctionProjectionResponse", "ClientCapabilities", "CatalogAttachRequest", "CatalogCreateRequest", @@ -245,6 +261,107 @@ class BindRequest(ArrowSerializableDataclass): argument_names: Annotated[list[str | None] | None, ArrowType(pa.list_(pa.string()))] = None +@dataclass(frozen=True, kw_only=True) +class TableFunctionProjectionRequest(_SqlRecord): + """Propose pure scalar outputs after bind, before modifying an execution plan. + + Expressions address the complete original worker output schema. Their result + fields are parallel by index; accepted fields append to that schema. Planning + allocates no execution resources and cannot consume rows or produce effects. + """ + + bind_call: Annotated[BindRequest, ArrowType(pa.binary())] + bind_result: Annotated[BindResponse, ArrowType(pa.binary())] + expressions: Annotated[list[bytes], ArrowType(pa.list_(pa.large_binary()))] + result_schema: Annotated[pa.Schema, ArrowType(pa.binary())] + + def __post_init__(self) -> None: + """Validate the source contract, offered profiles, owner and scalar records.""" + if not isinstance(self.bind_call, BindRequest) or self.bind_call.function_type != FunctionType.TABLE: + raise ValueError("scalar projection requires an ordinary table bind") + if self.bind_call.input_schema is not None or self.bind_call.schema_path is None: + raise ValueError("scalar projection requires a source table and explicit owner schema") + if not isinstance(self.bind_result, BindResponse): + raise TypeError("scalar projection requires the original BindResponse") + self.bind_result.__post_init__() + if any((self.bind_result.lookup_secret_types, self.bind_result.lookup_scopes, self.bind_result.lookup_names)): + raise ValueError("scalar projection cannot use a secret-scope bind response") + if not isinstance(self.expressions, list) or not 1 <= len(self.expressions) <= 1024: + raise ValueError("scalar projection requires between one and 1024 expressions") + if not isinstance(self.result_schema, pa.Schema) or len(self.result_schema) != len(self.expressions): + raise ValueError("scalar projection result fields must match its expressions") + if self.result_schema.metadata: + raise ValueError("scalar projection result schema cannot replace source schema metadata") + source = self.bind_result.output_schema + ascii_fold = str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") + names = [name.translate(ascii_fold) for name in [*source.names, *self.result_schema.names]] + if any(not name or "\0" in name for name in names) or len(set(names)) != len(names): + raise ValueError("scalar projection requires unique unambiguous source and result names") + context = None + for expression in self.expression_records(): + expression.validate_owner(self.bind_call.schema_path) + if ( + expression.profile != ROW_SCALAR_EXPRESSION + or expression.profile not in self.bind_result.projection_expression_profiles + ): + raise ValueError("scalar projection expression profile was not offered by this bind") + if context is not None and context != expression.evaluation_context: + raise ValueError("scalar projection expressions must share one evaluation context") + context = expression.evaluation_context + + def expression_records(self) -> list[SqlExpression]: + """Decode the canonical, bounded scalar records without interpreting SQL.""" + return [SqlExpression.deserialize_from_bytes(value) for value in self.expressions] + + +@dataclass(frozen=True, kw_only=True) +class TableFunctionProjectionResponse(_SqlRecord): + """Accept an ordered subset of proposed outputs, or decline without effects.""" + + accepted_indices: list[int] = field(default_factory=list) + bind_result: Annotated[BindResponse | None, ArrowType(pa.binary())] = None + + def __post_init__(self) -> None: + """Reject contradictory acceptance and malformed output indices.""" + indices = self.accepted_indices + if ( + not isinstance(indices, list) + or len(indices) > 1024 + or any(type(index) is not int or index < 0 for index in indices) + ): + raise ValueError("invalid accepted projection indices") + if indices != sorted(set(indices)): + raise ValueError("accepted projection indices must be strictly increasing") + if bool(indices) != (self.bind_result is not None): + raise ValueError("accepted projection indices and replacement bind result must occur together") + if self.bind_result is not None: + self.bind_result.__post_init__() + if any( + (self.bind_result.lookup_secret_types, self.bind_result.lookup_scopes, self.bind_result.lookup_names) + ): + raise ValueError("accepted scalar projection cannot request secret resolution") + + def validate_request(self, request: TableFunctionProjectionRequest) -> None: + """Check the complete append-only replacement before optimizer changes.""" + self.__post_init__() + request.__post_init__() + if self.bind_result is None: + return + if any(index >= len(request.expressions) for index in self.accepted_indices): + raise ValueError("accepted projection index is outside the proposal") + if self.bind_result.projection_expression_profiles != request.bind_result.projection_expression_profiles: + raise ValueError("accepted scalar projection cannot change the bind profile offer") + if self.bind_result.repeatability != request.bind_result.repeatability: + raise ValueError("accepted scalar projection cannot change bind repeatability") + source = request.bind_result.output_schema + expected = pa.schema( + [*source, *(request.result_schema.field(index) for index in self.accepted_indices)], + metadata=cast(Any, source.metadata), + ) + if not self.bind_result.output_schema.equals(expected, check_metadata=True): + raise ValueError("accepted scalar projection output schema differs from the requested append-only contract") + + @dataclass(frozen=True, slots=True, kw_only=True) class InitRequest(ArrowSerializableDataclass): """Consolidated init request for all function types. @@ -712,6 +829,8 @@ class ClientCapabilities(ArrowSerializableDataclass): catalogs: Companion catalog types the client can attach. can_stream: Whether the client can consume an unbounded streaming scan. filter_encodings: Filter representations understood by the client. + catalog: Optional offer of versioned catalog profiles. Absence selects none. + query_pushdown: Optional query encoding and semantic profile offer. Absence disables query execution. """ engine: str @@ -719,6 +838,8 @@ class ClientCapabilities(ArrowSerializableDataclass): catalogs: list[str] can_stream: bool filter_encodings: list[str] + catalog: Annotated[CatalogCapabilities | None, ArrowType(pa.binary())] = None + query_pushdown: ClientQueryCapabilities | None = None @dataclass(frozen=True, slots=True, kw_only=True) @@ -784,13 +905,14 @@ class TableCreateRequest(ArrowSerializableDataclass): columns: The columns of the table as a PyArrow schema serialized as bytes. on_conflict: Behavior when the object already exists ([`OnConflict`][]: ERROR/IGNORE/REPLACE). - not_null_constraints: Column indices with a NOT NULL constraint. - unique_constraints: Column-index groups with a UNIQUE constraint. - check_constraints: SQL CHECK constraint expressions. - primary_key_constraints: Column-index groups forming the primary key. - foreign_key_constraints: Serialized foreign-key constraint specs. + constraint_definitions: Required canonical ConstraintDefinition IPC list. + Empty means no constraints; nonempty requests ENFORCED + ALL_ROWS. transaction_opaque_data: Opaque DuckDB transaction handle (bytes); ``None`` when not inside a transaction. + comment: Optional creation comment; requires the table metadata profile. + tags: Optional creation tags; an empty map is an explicit request. + options: Optional typed literal options; requires the table options profile. + layout: Optional partition/sort policy; requires layout and expression profiles. """ attach_opaque_data: bytes @@ -798,17 +920,27 @@ class TableCreateRequest(ArrowSerializableDataclass): name: str columns: bytes # SerializedSchema on_conflict: OnConflict - not_null_constraints: Annotated[list[int], ArrowType(pa.list_(pa.int32()))] = field(default_factory=list) - unique_constraints: Annotated[list[list[int]], ArrowType(pa.list_(pa.list_(pa.int32())))] = field( - default_factory=list - ) - check_constraints: list[str] = field(default_factory=list) - primary_key_constraints: Annotated[list[list[int]], ArrowType(pa.list_(pa.list_(pa.int32())))] = field( - default_factory=list - ) - foreign_key_constraints: Annotated[list[bytes], ArrowType(pa.list_(pa.binary()))] = field(default_factory=list) + constraint_definitions: list[bytes] transaction_opaque_data: bytes | None = None + comment: str | None = None + tags: dict[str, str] | None = None + options: Annotated[TypedValueBatch | None, ArrowType(pa.binary())] = None + layout: Annotated[TableLayout | None, ArrowType(pa.binary())] = None + + def __post_init__(self) -> None: + """Reject missing, malformed or conflicting canonical definitions.""" + validate_column_metadata(pa.ipc.read_schema(pa.py_buffer(self.columns))) + constraint_definitions( + self.constraint_definitions, + CatalogObjectRef(kind="table", schema_path=self.schema_path, name=self.name), + ) + + def serialize_to_bytes(self) -> bytes: + """Recheck column metadata before a CREATE request reaches a provider.""" + self.__post_init__() + return ArrowSerializableDataclass.serialize_to_bytes(self) + # --------------------------------------------------------------------------- # Catalog response types @@ -1010,42 +1142,28 @@ class CopyFromFormatsResponse(_CatalogItemsResponseStub): @dataclass(frozen=True, slots=True, kw_only=True) class MacroCreateRequest(ArrowSerializableDataclass): - """Request for catalog_macro_create with `RecordBatch` for parameter defaults. - - Attributes: - attach_opaque_data: Opaque per-attach catalog session token (bytes) echoed - back so the worker can scope catalog state to the attachment. - schema_path: Raw identifier components of the schema containing the object. - name: Name of the target object/function. - macro_type: Whether this is a scalar or table macro. - parameters: Ordered list of parameter names. - definition: The SQL expression (scalar) or query (table). - on_conflict: Behavior when the object already exists ([`OnConflict`][]: - ERROR/IGNORE/REPLACE). - parameter_default_values: One-row `RecordBatch` where column names are - parameter names and values are typed defaults; ``None`` if no defaults. - arguments_schema: Optional Arrow schema (serialized as IPC bytes) with one - nullable field per parameter, in ``parameters`` order. Each field's type - is the parameter's default value type when known (else null), and the - ``vgi_doc`` field metadata key carries the parameter's description (UTF-8, - presence-only — omitted when undocumented). Mirrors the per-argument doc - channel functions expose. ``None`` when no per-parameter docs are - supplied. Built with ``vgi.argument_spec.macro_arguments_schema``. - transaction_opaque_data: Opaque DuckDB transaction handle (bytes); ``None`` - when not inside a transaction. - """ + """Create one complete macro family; the provider resolves expression defaults.""" attach_opaque_data: bytes schema_path: SchemaPath name: str macro_type: MacroType - parameters: list[str] - definition: str + overloads: list[MacroOverload] on_conflict: OnConflict - parameter_default_values: Annotated[pa.RecordBatch | None, ArrowType(pa.binary())] = None - arguments_schema: Annotated[pa.Schema | None, ArrowType(pa.binary())] = None transaction_opaque_data: bytes | None = None + def __post_init__(self) -> None: + """Validate the mandatory complete replacement family.""" + validate_macro_family(macro_type=self.macro_type.value, schema_path=self.schema_path, overloads=self.overloads) + + def serialize_to_bytes(self) -> bytes: + """Revalidate mutable overload families before sending a create request.""" + self.__post_init__() + result = ArrowSerializableDataclass.serialize_to_bytes(self) + if len(result) > MAX_MACRO_BYTES: + raise ValueError("macro record is too large") + return result + if TYPE_CHECKING: @@ -1114,37 +1232,27 @@ class CatalogContentsResponse(ArrowSerializableDataclass): @dataclass(frozen=True, slots=True, kw_only=True) class IndexCreateRequest(ArrowSerializableDataclass): - """Request for catalog_index_create. - - Attributes: - attach_opaque_data: Opaque per-attach catalog session token (bytes) echoed - back so the worker can scope catalog state to the attachment. - schema_path: Raw identifier components of the schema containing the object. - name: Name of the target object/function. - table_name: The name of the table this index is on. - index_type: The index type string (e.g., "ART", or empty for default). - constraint_type: The constraint enforcement type (NONE, UNIQUE, PRIMARY). - expressions: SQL expression strings defining the indexed expressions. For - column-based indexes these are column references (e.g., "col_a"); for - expression indexes, arbitrary SQL (e.g., "lower(col_a)"). - on_conflict: Behavior when the object already exists ([`OnConflict`][]: - ERROR/IGNORE/REPLACE). - options: Key-value index options (WITH clause). - transaction_opaque_data: Opaque DuckDB transaction handle (bytes); ``None`` - when not inside a transaction. - """ + """Create a typed index under a negotiated provider algorithm profile.""" attach_opaque_data: bytes schema_path: SchemaPath name: str - table_name: str - index_type: str = "" - constraint_type: IndexConstraintType = IndexConstraintType.NONE - expressions: list[str] = field(default_factory=list) + definition: Annotated[IndexDefinition, ArrowType(pa.binary())] on_conflict: OnConflict = OnConflict.ERROR - options: dict[str, str] = field(default_factory=dict) transaction_opaque_data: bytes | None = None + def __post_init__(self) -> None: + """Require the complete definition and a consistent owning schema.""" + self.definition.__post_init__() + OnConflict(self.on_conflict) + if not self.name or "\0" in self.name: + raise ValueError("index name must be a nonempty raw identifier") + + def serialize_to_bytes(self) -> bytes: + """Revalidate mutable nested records before dispatch.""" + self.__post_init__() + return super().serialize_to_bytes() + # --------------------------------------------------------------------------- # StreamState implementations @@ -2174,6 +2282,7 @@ def rehydrate(self, implementation: object) -> None: attach_plaintext=self._attach_pt, ), attach_opaque_data=attach_catalog_bytes(self._attach_pt), + transaction_opaque_data=worker._unwrap_transaction_for(self._call.init_call), ) # Re-derive pushdown filters before any fallback initial_state() call so # manual consumers see the same required/current state after HTTP rehydrate. @@ -2366,6 +2475,7 @@ def rehydrate(self, implementation: object) -> None: attach_plaintext=self._attach_pt, ), attach_opaque_data=attach_catalog_bytes(self._attach_pt), + transaction_opaque_data=worker._unwrap_transaction_for(self._call.init_call), ) self._auto_apply = func_cls._should_auto_apply_filters() if self._call.init_call.pushdown_filters is not None: @@ -3364,7 +3474,7 @@ class VgiProtocol(Protocol): # The wire name carries the major version, so an incompatible major is a # *different* protocol and therefore a 404 -- an answer every proxy, WAF and # load balancer understands without an Arrow parser, and one that lets - # ``vgi.v2`` and a future ``vgi.v3`` be served side by side while clients + # ``vgi.v2`` and ``vgi.v3`` be served side by side while clients # migrate. That matters here specifically: the DuckDB extension ships to # users and cannot be flag-dayed. # @@ -3372,13 +3482,17 @@ class VgiProtocol(Protocol): # name, which left the six implementations disagreeing four ways -- Python # ``VgiProtocol``, Java and C# ``VgiService``, Go the framework default # ``Service``, TypeScript ``vgi`` -- so no client could address them all. - protocol_name: ClassVar[str] = "vgi.v2" - protocol_version: ClassVar[str] = "2.2.0" + protocol_name: ClassVar[str] = "vgi.v3" + protocol_version: ClassVar[str] = "3.0.0" def bind(self, request: BindRequest) -> BindResponse: """Resolve output schema and validate arguments.""" ... + def table_function_projection(self, request: TableFunctionProjectionRequest) -> TableFunctionProjectionResponse: + """Accept optional scalar outputs before optimizer rewrite, or decline normally.""" + ... + def init(self, request: InitRequest) -> Stream[ProcessState, GlobalInitResponse]: """Initialize a function execution and return a processing stream.""" ... @@ -3542,6 +3656,16 @@ def catalog_transaction_rollback(self, attach_opaque_data: bytes, transaction_op """Rollback a transaction.""" ... + def catalog_query_prepare(self, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + """Authorize and describe the negotiated query or statement without executing it.""" + ... + + def catalog_query_release( + self, attach_opaque_data: bytes, prepared_query_token: bytes, transaction_opaque_data: bytes | None = None + ) -> None: + """Idempotently release an owned preparation; existing executions retain their lifetime.""" + ... + # ========== Catalog - Schemas ========== def catalog_schemas( @@ -3564,10 +3688,47 @@ def catalog_schema_create( comment: str | None = None, tags: dict[str, str] | None = None, transaction_opaque_data: bytes | None = None, + options: bytes | None = None, ) -> None: """Create a new schema.""" ... + def catalog_schema_option_definitions( + self, + attach_opaque_data: bytes, + path: SchemaPath, + transaction_opaque_data: bytes | None = None, + ) -> OptionDefinitions: + """Discover literal option definitions for an existing or proposed schema.""" + ... + + def catalog_schema_alter_metadata( + self, + attach_opaque_data: bytes, + path: SchemaPath, + changes: bytes, + ignore_not_found: bool = False, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Atomically apply a CatalogMetadataChanges patch under the selected DDL profile.""" + ... + + def catalog_schema_alter_options( + self, + attach_opaque_data: bytes, + path: SchemaPath, + changes: bytes, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Atomically SET/RESET schema options under the selected capability.""" + ... + + def catalog_dependencies( + self, attach_opaque_data: bytes, transaction_opaque_data: bytes | None = None + ) -> DependencySnapshot: + """Read a complete dependency graph under vgi.catalog.dependencies.read.v1.""" + ... + def catalog_schema_drop( self, attach_opaque_data: bytes, @@ -3654,6 +3815,42 @@ def catalog_table_create(self, request: TableCreateRequest) -> None: """Create a new table.""" ... + def catalog_table_constraints_apply(self, request: ConstraintsApplyRequest) -> ConstraintSnapshot: + """Atomically apply canonical changes at the expected table revision.""" + ... + + def catalog_table_option_definitions( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + transaction_opaque_data: bytes | None = None, + ) -> OptionDefinitions: + """Discover provider table-option definitions under the read profile.""" + ... + + def catalog_table_alter_options( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + changes: bytes, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Apply an IPC OptionChanges record atomically.""" + ... + + def catalog_table_alter_layout( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + changes: bytes, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Apply an IPC TableLayoutChange under its explicit existing-data mode.""" + ... + def catalog_table_drop( self, attach_opaque_data: bytes, @@ -3773,7 +3970,7 @@ def catalog_table_column_comment_set( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], comment: str | None = None, ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, @@ -3798,6 +3995,7 @@ def catalog_table_column_add( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, + column_path: list[str], column_definition: bytes, ignore_not_found: bool = False, if_column_not_exists: bool = False, @@ -3811,7 +4009,7 @@ def catalog_table_column_drop( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, if_column_exists: bool = False, cascade: bool = False, @@ -3825,7 +4023,7 @@ def catalog_table_column_rename( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], new_column_name: str, ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, @@ -3838,12 +4036,12 @@ def catalog_table_column_default_set( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, - column_name: str, - expression: str, + column_path: list[str], + expression: bytes, ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: - """Set the default value expression for a column.""" + """Set a column default using one canonical SqlExpression IPC record.""" ... def catalog_table_column_default_drop( @@ -3851,7 +4049,7 @@ def catalog_table_column_default_drop( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: @@ -3863,12 +4061,13 @@ def catalog_table_column_type_change( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, + column_path: list[str], column_definition: bytes, - expression: str | None = None, + expression: bytes | None = None, ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: - """Change the type of a column.""" + """Change a column type, optionally using canonical SqlExpression IPC.""" ... def catalog_table_not_null_drop( @@ -3876,7 +4075,7 @@ def catalog_table_not_null_drop( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: @@ -3888,7 +4087,7 @@ def catalog_table_not_null_set( attach_opaque_data: bytes, schema_path: SchemaPath, name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: @@ -3915,6 +4114,11 @@ def catalog_view_create( definition: str, on_conflict: OnConflict, transaction_opaque_data: bytes | None = None, + aliases: list[str] | None = None, + security: str = "regular", + comment: str | None = None, + tags: dict[str, str] | None = None, + column_comments: dict[str, str] | None = None, ) -> None: """Create a new view.""" ... @@ -3955,6 +4159,19 @@ def catalog_view_comment_set( """Set or clear the comment on a view.""" ... + def catalog_view_column_comment_set( + self, + attach_opaque_data: bytes, + schema_path: SchemaPath, + name: str, + column_path: list[str], + comment: str | None = None, + ignore_not_found: bool = False, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Set or clear a view output comment; column_path has one raw component.""" + ... + # ========== Catalog - Macros =========== def catalog_macro_get( @@ -4004,6 +4221,16 @@ def catalog_index_get( """Get information about an index. Returns 0 or 1 items.""" ... + def catalog_index_option_definitions( + self, + attach_opaque_data: bytes, + table: bytes, + algorithm: str, + transaction_opaque_data: bytes | None = None, + ) -> OptionDefinitions: + """Discover typed option definitions for an exact table and algorithm.""" + ... + def catalog_index_create(self, request: IndexCreateRequest) -> None: """Create a new index.""" ... diff --git a/vgi/protocol_version.txt b/vgi/protocol_version.txt index ccbccc3d..4a36342f 100644 --- a/vgi/protocol_version.txt +++ b/vgi/protocol_version.txt @@ -1 +1 @@ -2.2.0 +3.0.0 diff --git a/vgi/query_execution.py b/vgi/query_execution.py new file mode 100644 index 00000000..5451d02f --- /dev/null +++ b/vgi/query_execution.py @@ -0,0 +1,462 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""SDK-owned preparation receipts and the reserved catalog-query TABLE executor.""" + +from __future__ import annotations + +import hashlib +import time +from contextlib import suppress +from dataclasses import dataclass, replace +from typing import TYPE_CHECKING, Annotated, Any, cast + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, crypto +from vgi_rpc.rpc import AuthContext, OutputCollector, current_auth + +from vgi import attach_capabilities +from vgi.arguments import Arg +from vgi.catalog.query import ( + MAX_QUERY_TOKEN_BYTES, + QUERY_AUTOMATIC, + QUERY_EXECUTOR, + QUERY_EXPLICIT, + QUERY_STATEMENT, + STATEMENT_RESULT_KEY, + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, + _QueryRecord, + read_query_schema, + statement_result_kind, +) +from vgi.exceptions import CatalogCapabilityError, OpaqueDataNotRecognizedError +from vgi.invocation import BindResponse, FunctionType, GlobalInitResponse +from vgi.logical_types import require_logical_batch, require_logical_profiles, require_logical_values +from vgi.query_parameters import QueryParameters +from vgi.query_provider import CatalogQueryProvider, QueryExecution, QueryOwner, QueryPreparation +from vgi.table_function import BindParams, InitParams, ProcessParams, TableFunctionGenerator +from vgi.typed_values import TypedValueBatch, _validate_scalar + +if TYPE_CHECKING: + from vgi.protocol import BindRequest, InitRequest + from vgi.worker import Worker + +_TICKET_VERSION = 81 +_MAX_PREPARE_LIFETIME_MS = 60 * 60 * 1000 +_MAX_EXECUTION_LIFETIME_MS = 24 * 60 * 60 * 1000 + + +def _now_ms() -> int: + return time.time_ns() // 1_000_000 + + +def _provider(worker: Worker) -> CatalogQueryProvider: + provider = worker._get_catalog().query_provider + if not isinstance(provider, CatalogQueryProvider): + raise CatalogCapabilityError("catalog query", QUERY_EXPLICIT) + return provider + + +def owner_from_plaintext(plaintext: bytes, transaction: bytes | None = None) -> QueryOwner: + """Construct a resource owner only after the normal attachment authentication.""" + from vgi.worker import _attach_aad + + if len(plaintext) < 16 or transaction == b"": + raise OpaqueDataNotRecognizedError("query_owner") + auth = current_auth() or AuthContext.anonymous() + binding = bytearray(b"vgi.query.owner.v1\0") + for part in [_attach_aad(auth), plaintext, transaction or b""]: + binding.extend(len(part).to_bytes(8, "big")) + binding.extend(part) + return QueryOwner( + scope=hashlib.sha256(binding).digest(), + attach_opaque_data=plaintext[16:], + transaction_opaque_data=transaction, + auth=auth, + ) + + +def _owner(worker: Worker, attachment: bytes | None, transaction: bytes | None) -> QueryOwner: + if not attachment or transaction == b"": + raise OpaqueDataNotRecognizedError("query_owner") + plaintext = worker._unwrap_attach_full(attachment) + assert plaintext is not None + return owner_from_plaintext(plaintext, worker._unwrap_tx_opt(transaction, attachment)) + + +def negotiate( + worker: Worker, plaintext: bytes, offer: ClientQueryCapabilities | None, *, supports_transactions: bool +) -> QueryCapabilities | None: + """Select only support returned by the provider for the actual client build.""" + if offer is None: + return None + offer.__post_init__() + provider = worker._get_catalog().query_provider + if provider is None: + return None + available = provider.capabilities(owner=owner_from_plaintext(plaintext), offer=offer) + return available.select(offer, supports_transactions=supports_transactions) if available is not None else None + + +def _selected(worker: Worker, attachment: bytes) -> tuple[ClientQueryCapabilities, QueryCapabilities]: + plaintext = worker._open_attach_context(attachment, current_auth()) + assert plaintext is not None + agreement = attach_capabilities.query_agreement(plaintext) + if agreement is None: + raise CatalogCapabilityError("catalog query", QUERY_EXPLICIT) + return agreement + + +def _current( + worker: Worker, attachment: bytes | None, transaction: bytes | None +) -> tuple[CatalogQueryProvider, QueryOwner, QueryCapabilities]: + owner = _owner(worker, attachment, transaction) + assert attachment is not None + offer, selected = _selected(worker, attachment) + provider = _provider(worker) + available = provider.capabilities(owner=owner, offer=offer) + if ( + available is None + or available.select( + offer, supports_transactions=bool({QUERY_AUTOMATIC, QUERY_STATEMENT} & set(selected.semantic_profiles)) + ) + != selected + ): + raise CatalogCapabilityError("catalog query agreement changed; reattach", QUERY_EXPLICIT) + return provider, owner, selected + + +def _capability_digest(selected: QueryCapabilities) -> bytes: + return hashlib.sha256(selected.serialize_to_bytes()).digest() + + +def _fingerprint(preparation: QueryPreparation) -> bytes: + preparation.__post_init__() + request = preparation.request.serialize_to_bytes() + result = preparation.result.serialize_to_bytes() + return hashlib.sha256(len(request).to_bytes(8, "big") + request + result).digest() + + +@dataclass(frozen=True, kw_only=True) +class _QueryTicket(_QueryRecord): + kind: str + owner: bytes + capability: bytes + handle: bytes + contract: bytes + expires_at_epoch_ms: int + + def __post_init__(self) -> None: + if self.kind not in {"prepare", "execution"}: + raise ValueError("unknown query ticket kind") + if any(not isinstance(v, bytes) or len(v) != 32 for v in [self.owner, self.capability, self.contract]): + raise ValueError("invalid query ticket digest") + if not isinstance(self.handle, bytes) or not 0 < len(self.handle) <= 32768: + raise ValueError("invalid query resource handle") + if type(self.expires_at_epoch_ms) is not int or not 0 < self.expires_at_epoch_ms < 2**63: + raise ValueError("invalid query ticket expiry") + + +def _seal(worker: Worker, ticket: _QueryTicket) -> bytes: + payload = ticket.serialize_to_bytes() + if worker._signing_key is not None: + payload = crypto.seal_bytes( + payload, worker._signing_key, aad=b"vgi.query.ticket.v1\0" + ticket.owner, version=_TICKET_VERSION + ) + if len(payload) > MAX_QUERY_TOKEN_BYTES: + raise ValueError("query ticket exceeds the protocol size limit") + return payload + + +def _open( + worker: Worker, + payload: bytes, + owner: QueryOwner, + selected: QueryCapabilities, + kind: str, + *, + check_expiry: bool = True, +) -> _QueryTicket: + try: + if not isinstance(payload, bytes) or not 0 < len(payload) <= MAX_QUERY_TOKEN_BYTES: + raise ValueError("invalid ticket size") + if worker._signing_key is not None: + payload = crypto.open_bytes( + payload, worker._signing_key, aad=b"vgi.query.ticket.v1\0" + owner.scope, version=_TICKET_VERSION + ) + ticket = _QueryTicket.deserialize_from_bytes(payload) + if ticket.owner != owner.scope or ticket.capability != _capability_digest(selected) or ticket.kind != kind: + raise ValueError("query ticket scope mismatch") + if check_expiry and ticket.expires_at_epoch_ms <= _now_ms(): + raise ValueError("query ticket expired") + except (ValueError, TypeError, crypto.SealError) as error: + raise OpaqueDataNotRecognizedError("query_token") from error + return ticket + + +def prepare(worker: Worker, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + """Validate the attachment agreement, then seal an execution-free preparation.""" + request.__post_init__() + request.serialize_to_bytes() # Enforce the aggregate envelope bound on direct calls too. + provider, owner, selected = _current(worker, request.attach_opaque_data, request.transaction_opaque_data) + request.validate_agreement(selected) + logical_profiles = worker._effective_catalog_capabilities(request.attach_opaque_data).read + require_logical_profiles(read_query_schema(request.parameter_schema), selected_profiles=logical_profiles) + response = provider.prepare(owner=owner, request=request) + if not isinstance(response, CatalogQueryPrepareResult): + raise TypeError("provider must return a CatalogQueryPrepareResult") + try: + response.validate_request(request) + if response.status == "declined": + return response + require_logical_profiles(read_query_schema(response.output_schema), selected_profiles=logical_profiles) + assert response.prepared_query_token is not None and response.expires_at_epoch_ms is not None + now = _now_ms() + if not now < response.expires_at_epoch_ms <= now + _MAX_PREPARE_LIFETIME_MS: + raise ValueError("provider preparation expiry is outside the bounded lifetime") + preparation = QueryPreparation(request=request, result=response) + token = _seal( + worker, + _QueryTicket( + kind="prepare", + owner=owner.scope, + capability=_capability_digest(selected), + handle=response.prepared_query_token, + contract=_fingerprint(preparation), + expires_at_epoch_ms=response.expires_at_epoch_ms, + ), + ) + except Exception: + if response.prepared_query_token: + with suppress(Exception): + provider.release(owner=owner, recipe=response.prepared_query_token) + raise + return replace(response, prepared_query_token=token) + + +def release(worker: Worker, attachment: bytes, token: bytes, transaction: bytes | None) -> None: + """Permit owner cleanup after expiry or withdrawal, without accepting new execution.""" + owner = _owner(worker, attachment, transaction) + _, selected = _selected(worker, attachment) + ticket = _open(worker, token, owner, selected, "prepare", check_expiry=False) + _provider(worker).release(owner=owner, recipe=ticket.handle) + + +@dataclass(frozen=True) +class _QueryArguments: + prepared_query_token: Annotated[bytes, Arg(0)] + parameter_values: Annotated[bytes, Arg(1)] + + +@dataclass +class _QueryStreamState(ArrowSerializableDataclass): + position: int = 0 + closed: bool = False + + def __post_init__(self) -> None: + if type(self.position) is not int or not 0 <= self.position < 2**63 or type(self.closed) is not bool: + raise ValueError("invalid query continuation") + + +def _check_bind(request: BindRequest) -> None: + if request.function_type != FunctionType.TABLE or request.schema_path not in (None, []): + raise ValueError("catalog query executor is a schema-less TABLE function") + if len(request.arguments.positional) != 2 or request.arguments.named: + raise ValueError("catalog query executor requires exactly a receipt and one parameter batch") + for value in request.arguments.positional: + if value is None or not value.is_valid or value.type != pa.binary(): + raise ValueError("query executor arguments must be non-NULL binary values") + if any( + getattr(request, name) is not None + for name in ["input_schema", "settings", "secrets", "at_unit", "at_value", "copy_from", "copy_to"] + ): + raise ValueError( + "catalog query executor does not accept input, settings, secrets, time travel or COPY contexts" + ) + if request.resolved_secrets_provided: + raise ValueError("catalog query authority comes from its attachment") + + +def _bound( + worker: Worker, request: BindRequest, arguments: _QueryArguments +) -> tuple[CatalogQueryProvider, QueryOwner, QueryCapabilities, QueryPreparation, QueryParameters]: + _check_bind(request) + provider, owner, selected = _current(worker, request.attach_opaque_data, request.transaction_opaque_data) + ticket = _open(worker, arguments.prepared_query_token, owner, selected, "prepare") + preparation = provider.resolve(owner=owner, recipe=ticket.handle) + preparation.request.validate_agreement(selected) + if _fingerprint(preparation) != ticket.contract: + raise OpaqueDataNotRecognizedError("query_token") + parameters = QueryParameters.deserialize_from_bytes(arguments.parameter_values) + parameters.validate_prepared_schema(preparation.request.parameter_schema) + assert request.attach_opaque_data is not None + logical_profiles = worker._effective_catalog_capabilities(request.attach_opaque_data).read + require_logical_profiles(read_query_schema(preparation.result.output_schema), selected_profiles=logical_profiles) + require_logical_values(TypedValueBatch(parameters.batch), selected_profiles=logical_profiles) + return provider, owner, selected, preparation, parameters + + +def check_init(request: InitRequest) -> None: + """Reject alternate initialization paths before the generic secondary-init fast path.""" + if request.is_secondary or any( + getattr(request, name) is not None + for name in [ + "pushdown_filters", + "join_keys", + "split_tokens", + "row_limit", + "phase", + "finalize_state_id", + "init_opaque_data", + "substream_id", + "order_by_column_name", + "order_by_direction", + "order_by_null_order", + "order_by_limit", + "tablesample_percentage", + "tablesample_seed", + ] + ): + raise ValueError("catalog query executor does not accept splits, secondary readers or scan pushdown") + if request.projection_ids is not None and request.projection_ids != list(range(len(request.output_schema))): + raise ValueError("catalog query executor requires its complete output projection") + + +def executor_type(worker: Worker) -> type[TableFunctionGenerator[Any, Any]]: + """One internal executor per Worker, reused for all SQL and HTTP continuations.""" + if cached := worker.__dict__.get("_query_executor_type"): + return cast(type[TableFunctionGenerator[Any, Any]], cached) + + class QueryExecutor(TableFunctionGenerator[_QueryArguments, _QueryStreamState]): + FunctionArguments = _QueryArguments + + class Meta: + name = QUERY_EXECUTOR + projection_pushdown = False + filter_pushdown = False + auto_apply_filters = False + + @classmethod + def on_bind(cls, params: BindParams[_QueryArguments]) -> BindResponse: + _, _, _, preparation, _ = _bound(worker, params.bind_call, params.args) + return BindResponse(output_schema=read_query_schema(preparation.result.output_schema)) + + @classmethod + def on_init(cls, params: InitParams[_QueryArguments]) -> GlobalInitResponse: + check_init(params.init_call) + provider, owner, selected, preparation, parameters = _bound(worker, params.init_call.bind_call, params.args) + expected = read_query_schema(preparation.result.output_schema) + if not params.output_schema.equals(expected, check_metadata=True): + raise ValueError("query initialization changed the accepted output schema") + execution = provider.start( + owner=owner, preparation=preparation, parameters=parameters, execution_id=params.execution_id + ) + if not isinstance(execution, QueryExecution): + raise TypeError("provider must return a portable QueryExecution") + try: + if not _now_ms() < execution.expires_at_epoch_ms <= _now_ms() + _MAX_EXECUTION_LIFETIME_MS: + raise ValueError("provider execution expiry is outside the bounded lifetime") + token = _seal( + worker, + _QueryTicket( + kind="execution", + owner=owner.scope, + capability=_capability_digest(selected), + handle=execution.handle, + contract=_fingerprint(preparation), + expires_at_epoch_ms=execution.expires_at_epoch_ms, + ), + ) + except Exception: + with suppress(Exception): + provider.close(owner=owner, handle=execution.handle, reason="error") + raise + return GlobalInitResponse(execution_id=params.execution_id, max_workers=1, opaque_data=token) + + @classmethod + def initial_state(cls, params: ProcessParams[_QueryArguments]) -> _QueryStreamState: + return _QueryStreamState() + + @classmethod + def process( + cls, params: ProcessParams[_QueryArguments], state: _QueryStreamState, out: OutputCollector + ) -> None: + state.__post_init__() + if state.closed: + out.finish() + return + assert params.init_call is not None and params.init_response is not None + call = params.init_call.bind_call + owner = _owner(worker, call.attach_opaque_data, call.transaction_opaque_data) + assert call.attach_opaque_data is not None and params.init_response.opaque_data is not None + _, selected = _selected(worker, call.attach_opaque_data) + ticket = _open(worker, params.init_response.opaque_data, owner, selected, "execution", check_expiry=False) + provider = _provider(worker) + try: + _current(worker, call.attach_opaque_data, call.transaction_opaque_data) + if ticket.expires_at_epoch_ms <= _now_ms(): + raise OpaqueDataNotRecognizedError("query_token") + if any( + value is not None + for value in [params.current_pushdown_filters, params.if_none_match, params.if_modified_since] + ): + raise ValueError("catalog query executor rejects dynamic filters and result-cache validators") + batch = provider.read(owner=owner, handle=ticket.handle, position=state.position) + # The result kind is fixed by preparation and carried by the + # existing Arrow schema. No trailer, alternate RPC or mutable + # schema metadata is needed for command completion. + statement = ( + QUERY_STATEMENT in selected.semantic_profiles + and (params.output_schema.metadata or {}).get(STATEMENT_RESULT_KEY) is not None + ) + command = statement and statement_result_kind(params.output_schema) == "command" + if batch is None: + if command and state.position != 1: + raise ValueError("statement command requires exactly one completion row") + provider.close(owner=owner, handle=ticket.handle, reason="eos") + state.closed = True + out.finish() + return + if not isinstance(batch, pa.RecordBatch) or not batch.schema.equals( + params.output_schema, check_metadata=True + ): + raise ValueError("query result batch changed the accepted schema") + batch.validate(full=True) + if command: + if state.position != 0 or batch.num_rows != 1: + raise ValueError("statement command requires exactly one completion row") + count = batch.column(0)[0].as_py() + if count is not None and count < 0: + raise ValueError("statement affected_rows must not be negative") + require_logical_batch( + batch, + selected_profiles=worker._effective_catalog_capabilities(call.attach_opaque_data).read, + ) + for i, definition in enumerate(batch.schema): + for value in batch.column(i): + _validate_scalar(value, definition) + out.emit(batch) + state.position += 1 + except Exception: + state.closed = True + with suppress(Exception): + provider.close(owner=owner, handle=ticket.handle, reason="error") + raise + + @classmethod + def on_cancel(cls, params: ProcessParams[_QueryArguments], state: _QueryStreamState) -> None: + if state.closed: + return + assert params.init_call is not None and params.init_response is not None + call = params.init_call.bind_call + owner = _owner(worker, call.attach_opaque_data, call.transaction_opaque_data) + assert call.attach_opaque_data is not None and params.init_response.opaque_data is not None + _, selected = _selected(worker, call.attach_opaque_data) + ticket = _open(worker, params.init_response.opaque_data, owner, selected, "execution", check_expiry=False) + _provider(worker).close(owner=owner, handle=ticket.handle, reason="cancel") + state.closed = True + + worker.__dict__["_query_executor_type"] = QueryExecutor + return QueryExecutor diff --git a/vgi/query_parameters.py b/vgi/query_parameters.py new file mode 100644 index 00000000..198ebb67 --- /dev/null +++ b/vgi/query_parameters.py @@ -0,0 +1,173 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Typed query parameters carried entirely by a one-row Arrow batch. + +The schema fixes placeholder mapping and preserves recursive logical metadata. +Preparation binds this schema, not a set of values. Every execution must supply +the same schema, including metadata, with exactly one row of values. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Literal, Self, cast + +import pyarrow as pa + +from vgi.typed_values import TypedValueBatch + +PARAMETER_MODE_KEY = b"vgi.query.parameters.v1" +ParameterMode = Literal["named", "positional"] +MAX_PARAMETER_BYTES = 8 * 1024 * 1024 +MAX_PARAMETER_SCHEMA_BYTES = 1024 * 1024 +MAX_PARAMETERS = 1024 + + +def _validate_field_metadata(field: pa.Field[Any], depth: int = 0) -> None: + if depth > 64: + raise ValueError("query parameter type nesting is too deep") + if not field.equals(field.with_metadata(cast(Any, field.metadata)), check_metadata=True): + raise ValueError("query parameter field metadata keys must be unique") + dtype = field.type + for i in range(dtype.num_fields): + _validate_field_metadata(dtype.field(i), depth + 1) + if pa.types.is_dictionary(dtype): + _validate_field_metadata(pa.field("value", dtype.value_type), depth + 1) + elif isinstance(dtype, pa.BaseExtensionType): + _validate_field_metadata(pa.field("storage", dtype.storage_type), depth + 1) + + +def validate_parameter_schema(schema: pa.Schema) -> ParameterMode: + """Validate mapping without interpreting or changing logical-type metadata. + + Positional parameters use ordered field names ``1`` through ``N``. Named + parameters use their exact unprefixed placeholder names. Dialect-specific + resolution, including case equivalence, is checked by the query provider. + Repeated occurrences of one placeholder refer to one field. A request may + not mix named and positional mapping modes. + """ + if not isinstance(schema, pa.Schema): + raise TypeError("query parameter schema must be an Arrow Schema") + if not schema.equals(schema.with_metadata(schema.metadata), check_metadata=True): + raise ValueError("query parameter schema metadata keys must be unique") + for field in schema: + _validate_field_metadata(field) + mode = (schema.metadata or {}).get(PARAMETER_MODE_KEY) + if mode not in {b"named", b"positional"}: + raise ValueError("query parameter schema requires vgi.query.parameters.v1 named or positional metadata") + if len(schema) > MAX_PARAMETERS: + raise ValueError("too many query parameters") + if len(set(schema.names)) != len(schema): + raise ValueError("query parameter names must be unique") + if any(not name or "\0" in name for name in schema.names): + raise ValueError("query parameter names must be nonempty and contain no NUL") + if mode == b"positional": + if schema.names != [str(i + 1) for i in range(len(schema))]: + raise ValueError("positional query parameter fields must be ordered 1 through N") + return "positional" + if any(name[0] in "$:@?" or name.isdecimal() for name in schema.names): + raise ValueError("named query parameter fields require unprefixed nonnumeric names") + return "named" + + +def read_parameter_schema(payload: bytes) -> pa.Schema: + """Decode one bounded schema message; reject trailing bytes or batches.""" + if not isinstance(payload, bytes) or not 0 < len(payload) <= MAX_PARAMETER_SCHEMA_BYTES: + raise ValueError("invalid query parameter schema size") + source = pa.BufferReader(payload) + schema = pa.ipc.read_schema(cast(Any, source)) + if source.tell() != len(payload): + raise ValueError("query parameter schema has trailing data") + validate_parameter_schema(schema) + return schema + + +@dataclass(frozen=True, slots=True) +class QueryParameters: + """One execution's typed values, with a self-describing placeholder map. + + Typed NULL is a supplied parameter. Missing fields, NULL IPC and zero rows + are not parameter sets. Zero parameters are an explicit one-row, zero-column + batch with a mapping mode. No inference, casts, key folding or SQL text + substitution occurs in this carrier. + """ + + values: TypedValueBatch + + def __post_init__(self) -> None: + """Require a fully validated batch and the versioned mapping metadata.""" + if not isinstance(self.values, TypedValueBatch): + raise TypeError("query parameters require a TypedValueBatch") + self.values.__post_init__() + validate_parameter_schema(self.schema) + + @property + def schema(self) -> pa.Schema: + """Return all schema and recursive field metadata unchanged.""" + return self.values.schema + + @property + def batch(self) -> pa.RecordBatch: + """Return the one-row execution batch.""" + return self.values.batch + + @property + def mode(self) -> ParameterMode: + """Return the placeholder mapping carried by the schema.""" + return validate_parameter_schema(self.schema) + + def schema_bytes(self) -> bytes: + """Serialize the schema alone for preparation, without parameter values.""" + payload = self.schema.serialize().to_pybytes() + if len(payload) > MAX_PARAMETER_SCHEMA_BYTES: + raise ValueError("query parameter schema is too large") + return payload + + def validate_prepared_schema(self, expected: pa.Schema | bytes) -> None: + """Require a new preparation for any schema or metadata change.""" + if isinstance(expected, bytes): + expected = read_parameter_schema(expected) + validate_parameter_schema(expected) + self.values.validate_schema(expected) + + def serialize_to_bytes(self) -> bytes: + """Write a bounded, complete one-batch IPC stream.""" + payload = self.values.serialize_to_bytes() + if len(payload) > MAX_PARAMETER_BYTES: + raise ValueError("query parameter batch is too large") + self.schema_bytes() + return payload + + @classmethod + def deserialize_from_bytes(cls, payload: bytes, ipc_validation: Any = None) -> Self: + """Validate size before decoding and preserve typed NULLs and metadata.""" + if not isinstance(payload, bytes) or not 0 < len(payload) <= MAX_PARAMETER_BYTES: + raise ValueError("invalid query parameter batch size") + result = cls(TypedValueBatch.deserialize_from_bytes(payload)) + result.schema_bytes() + return result + + @classmethod + def from_batch(cls, batch: pa.RecordBatch, *, mode: ParameterMode) -> Self: + """Add a mapping mode without replacing other metadata or field types. + + An existing conflicting mode is an error, not an implicit rebinding. + The caller supplies positional field names explicitly. + """ + if mode not in {"named", "positional"}: + raise ValueError("unknown query parameter mode") + if not batch.schema.equals(batch.schema.with_metadata(batch.schema.metadata), check_metadata=True): + raise ValueError("query parameter schema metadata keys must be unique") + metadata = dict(batch.schema.metadata or {}) + existing = metadata.get(PARAMETER_MODE_KEY) + if existing is not None and existing != mode.encode(): + raise ValueError("query parameter mode conflicts with schema metadata") + metadata[PARAMETER_MODE_KEY] = mode.encode() + return cls(TypedValueBatch(batch.replace_schema_metadata(metadata))) + + @classmethod + def empty(cls, *, mode: ParameterMode = "positional") -> Self: + """Represent one execution with no parameters, retaining mapping mode.""" + if mode not in {"named", "positional"}: + raise ValueError("unknown query parameter mode") + return cls(TypedValueBatch.empty(metadata={PARAMETER_MODE_KEY: mode.encode()})) diff --git a/vgi/query_provider.py b/vgi/query_provider.py new file mode 100644 index 00000000..5b82822e --- /dev/null +++ b/vgi/query_provider.py @@ -0,0 +1,127 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""Provider boundary for catalog queries over the ordinary VGI TABLE lifecycle. + +Providers own logical-catalog authorization, backend transactions, portable +preparations, execution owners and bounded cleanup. No backend cursor is ever +serialized into a client continuation. An owner loss is an execution error, +never permission to execute the query again. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import pyarrow as pa +from vgi_rpc.rpc import AuthContext + +from vgi.catalog.query import ( + CatalogQueryPrepareRequest, + CatalogQueryPrepareResult, + ClientQueryCapabilities, + QueryCapabilities, +) +from vgi.query_parameters import QueryParameters + + +@dataclass(frozen=True, kw_only=True) +class QueryOwner: + """Current authenticated owner, with provider attachment/transaction bytes. + + ``scope`` is an SDK-derived owner key, including the framework attachment + identity, authentication identity and transaction. Providers bind every + stored resource to it and recheck current authorization on every operation. + It does not replace access checks or represent a catalog/data version. + """ + + scope: bytes + attach_opaque_data: bytes + transaction_opaque_data: bytes | None + auth: AuthContext + + +@dataclass(frozen=True, kw_only=True) +class QueryPreparation: + """Immutable preparation reloaded by its provider-owned receipt. + + The result token is the provider's private lookup recipe here. The SDK seals + it before returning it on the wire. SQL, schemas and result metadata must + survive reconnects without retaining a process-local relation or cursor. + """ + + request: CatalogQueryPrepareRequest + result: CatalogQueryPrepareResult + + def __post_init__(self) -> None: + """Require a complete accepted result for exactly this request.""" + if self.result.status != "accepted": + raise ValueError("stored query preparation must be accepted") + self.result.validate_request(self.request) + + +@dataclass(frozen=True, kw_only=True) +class QueryExecution: + """Serializable owner/fragment locator with its own bounded lifetime.""" + + handle: bytes + expires_at_epoch_ms: int + + +class CatalogQueryProvider: + """Optional provider API; these methods are not new streaming RPCs. + + Implementations must bound preparation/execution count, bytes and lifetimes. + ``prepare`` and ``resolve`` must not run a result query. ``start`` executes + once per successful invocation; neither ambiguous failures nor owner loss + are retried. Reads either return the identical batch for a repeated position + or reject replay. Closing/releasing resources is idempotent for their owner. + + Transaction end, attachment revocation, normal EOS, errors and cancellation + close executions. Since cancellation is best effort, expired resources must + also be reclaimed without requiring a well-behaved client. + + The optional vgi.remote.statement.v1 profile reuses this lifecycle for one + explicitly requested transactional DML/DDL statement. Preparation and bind + remain effect-free. Providers must reject scripts, transaction/session + control and nontransactional effects before execution. Statement execution + and result failures, cancellation and owner loss invalidate the entire + transaction; commit must also reject an execution that has not reached EOS. + A normal EOS completes the statement but does not commit its transaction. + Providers own these checks and backend rollback through the existing + transaction methods; the generic executor cannot infer backend effects. + """ + + def capabilities(self, *, owner: QueryOwner, offer: ClientQueryCapabilities) -> QueryCapabilities | None: + """Return support certified for this client build, without executing SQL.""" + return None + + def prepare(self, *, owner: QueryOwner, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + """Parse, authorize and describe; only legitimate placement failures decline.""" + raise NotImplementedError("catalog query preparation is not implemented") + + def resolve(self, *, owner: QueryOwner, recipe: bytes) -> QueryPreparation: + """Reload immutable, authorized preparation metadata without executing it.""" + raise NotImplementedError("catalog query receipt resolution is not implemented") + + def start( + self, + *, + owner: QueryOwner, + preparation: QueryPreparation, + parameters: QueryParameters, + execution_id: bytes, + ) -> QueryExecution: + """Revalidate schema/policy/snapshot, then start one fresh execution.""" + raise NotImplementedError("catalog query execution is not implemented") + + def read(self, *, owner: QueryOwner, handle: bytes, position: int) -> pa.RecordBatch | None: + """Read one ordered batch or EOS; never reconstruct an execution by rerunning SQL.""" + raise NotImplementedError("catalog query reading is not implemented") + + def close(self, *, owner: QueryOwner, handle: bytes, reason: str) -> None: + """Idempotently interrupt and release this owner's execution resources.""" + raise NotImplementedError("catalog query cleanup is not implemented") + + def release(self, *, owner: QueryOwner, recipe: bytes) -> None: + """Release a preparation without canceling executions already started from it.""" + raise NotImplementedError("catalog query release is not implemented") diff --git a/vgi/rpc_server.py b/vgi/rpc_server.py index 23b34072..9d86785a 100644 --- a/vgi/rpc_server.py +++ b/vgi/rpc_server.py @@ -11,7 +11,7 @@ What a server hosts, in reflection order: -1. The worker's own protocol (``vgi.v2``, or the worker's ``protocol_class``). +1. The worker's own protocol (``vgi.v3``, or the worker's ``protocol_class``). 2. The worker's [`hosted_protocols`][vgi.worker.Worker.hosted_protocols], in the order returned — on **every** transport. 3. ``vgi_rpc.Reflection.v1`` — on every transport unless ``describe=False`` @@ -22,9 +22,9 @@ configured explicitly *and* the worker can issue grants. See [`vgi.attach_ticket`][vgi.attach_ticket]. -Extra protocols cannot change ``vgi.v2`` behaviour: vgi-rpc routes every +Extra protocols cannot change ``vgi.v3`` behaviour: vgi-rpc routes every request on its ``vgi_rpc.protocol`` key, with no fallback to the primary, so a -client that only ever names ``vgi.v2`` (the DuckDB extension) dispatches +client that only ever names ``vgi.v3`` (the DuckDB extension) dispatches exactly as it would against a single-protocol server. """ diff --git a/vgi/schema_path.py b/vgi/schema_path.py index 8e1c672f..c1ebc903 100644 --- a/vgi/schema_path.py +++ b/vgi/schema_path.py @@ -12,6 +12,15 @@ SchemaKey: TypeAlias = tuple[str, ...] # noqa: UP040 +def validate_default_schema(default_schema: str | None) -> None: + """Validate an optional raw root identifier without inventing a default.""" + if default_schema is not None: + if not isinstance(default_schema, str): + raise TypeError("default_schema must be a string or None") + if not default_schema or "\x00" in default_schema: + raise ValueError("default_schema must be a nonempty identifier without NUL") + + def schema_path_key(path: Sequence[str]) -> SchemaKey: """Return a case-insensitive structural lookup key for a schema path. diff --git a/vgi/secret_protocol.py b/vgi/secret_protocol.py index b5e46b17..767ea4fe 100644 --- a/vgi/secret_protocol.py +++ b/vgi/secret_protocol.py @@ -133,7 +133,7 @@ class VgiSecretProtocol(Protocol): enforced as an exact major+minor match at the dispatch boundary. """ - # Its own name and its own major, independent of ``vgi.v2``: this contract + # Its own name and its own major, independent of ``vgi.v3``: this contract # versions on a separate cadence, which is why ``protocol_version_override`` # exists on the client side at all. protocol_name: ClassVar[str] = "vgi.secret.v1" diff --git a/vgi/table_function.py b/vgi/table_function.py index cc333157..b14e135d 100644 --- a/vgi/table_function.py +++ b/vgi/table_function.py @@ -52,7 +52,7 @@ if TYPE_CHECKING: from vgi.catalog.catalog_interface import ColumnStatistics - from vgi.protocol import BindRequest, InitRequest + from vgi.protocol import BindRequest, InitRequest, TableFunctionProjectionRequest, TableFunctionProjectionResponse from vgi.table_filter_pushdown import PushdownFilters _ON_CANCEL_CAVEATS = """\ @@ -572,6 +572,8 @@ class BindParams[TArgs]: auth_context: Authentication context for the caller. attach_opaque_data: The catalog's attach bytes, unwrapped by the framework (shard-UUID prefix stripped). None without an ATTACH. + transaction_opaque_data: The provider's transaction bytes, authenticated + and unwrapped by the worker. The bind request retains its sealed token. """ @@ -583,6 +585,7 @@ class BindParams[TArgs]: storage: BoundStorage | None = None auth_context: AuthContext = AuthContext.anonymous() attach_opaque_data: bytes | None = None + transaction_opaque_data: bytes | None = None @property def at_unit(self) -> str | None: @@ -616,6 +619,8 @@ class InitParams[TArgs]: auth_context: Authentication context for the caller. attach_opaque_data: The catalog's attach bytes, unwrapped by the framework (uuid prefix stripped). None without an ATTACH. + transaction_opaque_data: The provider's transaction bytes, authenticated + and unwrapped by the worker. The init request retains its sealed token. split_payloads: Verified split payloads when this init redeems splits, or None otherwise. The framework has already opened and stripped the token @@ -635,6 +640,7 @@ class InitParams[TArgs]: storage: BoundStorage auth_context: AuthContext = AuthContext.anonymous() attach_opaque_data: bytes | None = None + transaction_opaque_data: bytes | None = None split_payloads: list[bytes] | None = None @property @@ -676,6 +682,8 @@ class ProcessParams[TArgs]: source order under parallel ingest. None for every other call path. attach_opaque_data: The catalog's attach bytes, unwrapped by the framework (uuid prefix stripped). None without an ATTACH. + transaction_opaque_data: The provider's transaction bytes, authenticated + and unwrapped on this turn. Never serialized into continuation state. if_none_match: Conditional-revalidation validator (client's stored ETag). Set when the client holds a stale-but-revalidatable cached result and asks the worker to confirm freshness cheaply; a worker @@ -709,6 +717,7 @@ class ProcessParams[TArgs]: batch_index: int | None = None attach_opaque_data: bytes | None = None + transaction_opaque_data: bytes | None = None # Conditional-revalidation validators (client cache -> worker). Set when the # client holds a stale-but-revalidatable cached result and asks the worker to @@ -1088,6 +1097,7 @@ def _make_bind_params( auth_context: AuthContext | None = None, execution_id: bytes | None = None, attach_plaintext: bytes | None = None, + transaction_plaintext: bytes | None = None, ) -> BindParams[TArgs]: """Construct BindParams from a BindRequest. @@ -1104,6 +1114,7 @@ def _make_bind_params( or None on call paths without one. attach_plaintext: Full framework attach plaintext (uuid || catalog bytes), or None without an ATTACH. + transaction_plaintext: Worker-verified provider transaction bytes, or None. Returns: The constructed BindParams. @@ -1136,6 +1147,7 @@ def _make_bind_params( else None, auth_context=auth_context if auth_context is not None else AuthContext.anonymous(), attach_opaque_data=attach_catalog_bytes(attach_plaintext), + transaction_opaque_data=transaction_plaintext, ) # ------------------------------------------------------------------ @@ -1180,6 +1192,7 @@ def bind( *, ctx: CallContext | None = None, attach_plaintext: bytes | None = None, + transaction_plaintext: bytes | None = None, ) -> BindResponse: """Bind protocol entry point. Do not override; use ``on_bind()``. @@ -1198,6 +1211,7 @@ def bind( input: The bind request from the client. ctx: Call context carrying the caller's auth, if any. attach_plaintext: Full framework attach plaintext, or None. + transaction_plaintext: Worker-verified provider transaction bytes, or None. Returns: The BindResponse from ``on_bind()``, or a secret-scope request when @@ -1205,7 +1219,9 @@ def bind( """ auth = ctx.auth if ctx is not None else AuthContext.anonymous() - params = cls._make_bind_params(input, auth_context=auth, attach_plaintext=attach_plaintext) + params = cls._make_bind_params( + input, auth_context=auth, attach_plaintext=attach_plaintext, transaction_plaintext=transaction_plaintext + ) if input.input_schema is not None: cls._validate_arg_type_bounds(cls.FunctionArguments, params.args, input.input_schema) @@ -1217,6 +1233,21 @@ def bind( return result + @classmethod + def on_projection( + cls, params: BindParams[TArgs], request: TableFunctionProjectionRequest + ) -> TableFunctionProjectionResponse: + """Decline optional scalar outputs unless the provider explicitly qualifies them. + + Overrides must bind without fetching rows or allocating execution resources. + Accepted opaque data replaces the complete prior bind state and follows + ordinary init/stream lifetime. A later execution error never permits a + fallback retry. Unaccepted expressions and filters remain client-owned. + """ + from vgi.protocol import TableFunctionProjectionResponse + + return TableFunctionProjectionResponse() + @classmethod def on_init( cls, @@ -1244,6 +1275,7 @@ def global_init( *, ctx: CallContext | None = None, attach_plaintext: bytes | None = None, + transaction_plaintext: bytes | None = None, signing_key: bytes | None = None, current_anchor: bytes | None = None, ) -> GlobalInitResponse: @@ -1253,6 +1285,7 @@ def global_init( input: The init request from the client. ctx: Call context carrying the caller's auth, if any. attach_plaintext: Full framework attach plaintext, or None. + transaction_plaintext: Worker-verified provider transaction bytes, or None. signing_key: The worker's AEAD key, passed down so a split token can be opened. Owned by the worker, not by this class. current_anchor: The catalog version a split token must still name, or @@ -1285,6 +1318,7 @@ def global_init( storage=BoundStorage(cls.storage, execution_id, request=input, attach_plaintext=attach_plaintext), auth_context=auth, attach_opaque_data=attach_catalog_bytes(attach_plaintext), + transaction_opaque_data=transaction_plaintext, split_payloads=cls._verify_split_tokens(input, auth, signing_key, current_anchor), ) diff --git a/vgi/transactor/protocol.py b/vgi/transactor/protocol.py index d1c667b6..11290111 100644 --- a/vgi/transactor/protocol.py +++ b/vgi/transactor/protocol.py @@ -16,8 +16,10 @@ from __future__ import annotations -from typing import Protocol +from typing import Annotated, Protocol +import pyarrow as pa +from vgi_rpc import ArrowType from vgi_rpc.rpc import ExchangeState, ProducerState, Stream from vgi.schema_path import SchemaPath @@ -61,7 +63,7 @@ def insert( tx_id: bytes, table_name: str, schema_path: SchemaPath | None = None, - result_mode: WriteResultMode = "count", + result_mode: Annotated[WriteResultMode, ArrowType(pa.string())] = "count", ) -> Stream[ExchangeState]: """Insert rows into a table via lockstep exchange.""" ... @@ -72,7 +74,7 @@ def delete( tx_id: bytes, table_name: str, schema_path: SchemaPath | None = None, - result_mode: WriteResultMode = "count", + result_mode: Annotated[WriteResultMode, ArrowType(pa.string())] = "count", ) -> Stream[ExchangeState]: """Delete rows from a table via lockstep exchange.""" ... @@ -84,7 +86,7 @@ def update( table_name: str, schema_path: SchemaPath | None = None, columns: list[str] | None = None, - result_mode: WriteResultMode = "count", + result_mode: Annotated[WriteResultMode, ArrowType(pa.string())] = "count", ) -> Stream[ExchangeState]: """Update rows in a table via lockstep exchange.""" ... diff --git a/vgi/typed_values.py b/vgi/typed_values.py new file mode 100644 index 00000000..bbb9241a --- /dev/null +++ b/vgi/typed_values.py @@ -0,0 +1,135 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""One-row Arrow values that retain field and schema metadata. + +Use this carrier when each column is one typed argument or option. A typed NULL +is a present value; an absent field is a different operation. Interpretation of +logical-type metadata belongs to the consuming protocol and its capabilities. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Self, cast + +import pyarrow as pa + + +def _validate_scalar(value: pa.Scalar[Any] | None, definition: pa.Field[Any]) -> None: + """Check declared nullability, including children of present nested values.""" + if isinstance(value, pa.UnionScalar): + # PyArrow's stub declares type_code as str; the runtime returns int. + child = value.type[value.type.type_codes.index(cast(int, value.type_code))] + _validate_scalar(value.value, child) + return + if isinstance(value, (pa.DictionaryScalar, pa.ExtensionScalar, pa.RunEndEncodedScalar)): + decoded = cast("pa.Scalar[Any] | None", value.value) + _validate_scalar(decoded, definition.with_type(decoded.type) if decoded is not None else definition) + return + if value is None or not value.is_valid: + if not definition.nullable: + raise ValueError(f"NULL supplied for non-nullable field {definition.name!r}") + return + if isinstance(value, pa.StructScalar): + for index, child in enumerate(value.type): + _validate_scalar(cast(Any, value)[index], child) + elif isinstance(value, pa.MapScalar): + # MapScalar inherits ListScalar, but has key/item fields rather than a + # list's value_field, so it must be handled first. + assert value.values is not None + for pair in value.values: + _validate_scalar(pair[0], value.type.key_field) + _validate_scalar(pair[1], value.type.item_field) + elif isinstance( + value, (pa.ListScalar, pa.LargeListScalar, pa.FixedSizeListScalar, pa.ListViewScalar, pa.LargeListViewScalar) + ): + assert value.values is not None + for item in value.values: + _validate_scalar(item, value.type.value_field) + + +@dataclass(frozen=True, slots=True) +class TypedValueBatch: + """Exactly one row of typed values, with the original Arrow schema intact. + + Field names must be unique, nonempty, and contain no NUL. Zero columns are + valid and still represent one set of values. This class preserves metadata; + it does not infer or claim support for the logical types described by it. + + Attributes: + batch: The single record batch containing the values. + """ + + batch: pa.RecordBatch + + def __post_init__(self) -> None: + """Validate shape, names, Arrow buffers, and declared nullability.""" + if not isinstance(self.batch, pa.RecordBatch): + raise TypeError("typed values require an Arrow RecordBatch") + if self.batch.num_rows != 1: + raise ValueError("typed values must contain exactly one row") + if len(set(self.schema.names)) != self.batch.num_columns: + raise ValueError("typed value field names must be unique") + if any(not name or "\x00" in name for name in self.schema.names): + raise ValueError("typed value field names must be nonempty and contain no NUL") + self.batch.validate(full=True) + for index, definition in enumerate(self.schema): + _validate_scalar(self.batch.column(index)[0], definition) + + @property + def schema(self) -> pa.Schema: + """Return the original schema, including recursive field metadata.""" + return self.batch.schema + + def validate_schema(self, expected: pa.Schema) -> None: + """Require identical names, order, types, nullability, and metadata. + + Values can change between executions of a prepared operation. A schema + change requires an explicit new binding; this helper performs no casts. + """ + if not self.schema.equals(expected, check_metadata=True): + raise ValueError("typed values do not match the expected schema and metadata") + + def serialize_to_bytes(self) -> bytes: + """Encode one batch as an IPC stream, preserving its complete schema.""" + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, self.schema) as writer: + writer.write_batch(self.batch) + return sink.getvalue().to_pybytes() + + @classmethod + def deserialize_from_bytes(cls, payload: bytes, ipc_validation: Any = None) -> Self: + """Decode exactly one batch, rejecting extra batches and trailing bytes. + + ``ipc_validation`` is accepted for vgi-rpc's custom-codec interface. + Typed values always receive full Arrow and nullability validation. + Transport request-size budgets remain the caller's responsibility. + """ + source = pa.BufferReader(payload) + try: + with pa.ipc.open_stream(source) as reader: + try: + batch = reader.read_next_batch() + except StopIteration as exc: + raise ValueError("typed value IPC contains no batch") from exc + try: + reader.read_next_batch() + except StopIteration: + pass + else: + raise ValueError("typed value IPC must contain exactly one batch") + if source.tell() != len(payload): + raise ValueError("typed value IPC has trailing data") + except OSError as exc: + # PyArrow can report a truncated in-memory IPC body as OSError. + # This is malformed caller data, not a filesystem/server failure. + raise ValueError("typed value IPC is malformed or truncated") from exc + return cls(batch) + + @classmethod + def empty(cls, *, metadata: Mapping[bytes, bytes] | None = None) -> Self: + """Create a zero-column batch that still contains one set of values.""" + array = cast(pa.StructArray, pa.array([{}], type=pa.struct([]))) + batch = pa.RecordBatch.from_struct_array(array) + return cls(batch.replace_schema_metadata(dict(metadata) if metadata is not None else None)) diff --git a/vgi/worker.py b/vgi/worker.py index 8b89003f..103121ac 100644 --- a/vgi/worker.py +++ b/vgi/worker.py @@ -83,26 +83,86 @@ class MyWorker(Worker): import pyarrow as pa from vgi_rpc.rpc import AuthContext, CallContext, Stream, current_auth, serve_stdio -from vgi import attach_header +from vgi import attach_capabilities, attach_header from vgi._redact import short_hash from vgi.aggregate_function import AggregateBindParams, AggregateFunction from vgi.argument_spec import ArgumentSpec, extract_argument_specs from vgi.arguments import Arguments from vgi.catalog import CatalogInterface from vgi.catalog.attach_option import AttachOptionSpec, extract_attach_option_specs +from vgi.catalog.capabilities import ( + CATALOG_DEPENDENCIES_READ, + COLUMN_NESTED_ADD, + COLUMN_NESTED_COMMENT, + COLUMN_NESTED_DEFAULT, + COLUMN_NESTED_DROP, + COLUMN_NESTED_NULLABILITY, + COLUMN_NESTED_RENAME, + COLUMN_NESTED_TYPE, + CONSTRAINT_CREATE, + CONSTRAINT_ENFORCE, + CONSTRAINT_MUTATE, + CONSTRAINT_RECORDS_READ, + INDEX_DEFINITION_CREATE, + INDEX_DEFINITION_READ, + INDEX_DROP, + ROW_SCALAR_EXPRESSION, + SCHEMA_LIFECYCLE, + SCHEMA_METADATA_ALTER, + SCHEMA_OPTIONS_ALTER, + SCHEMA_OPTIONS_CREATE, + SCHEMA_OPTIONS_READ, + TABLE_LAYOUT_ALTER_FUTURE, + TABLE_LAYOUT_ALTER_REWRITE, + TABLE_LAYOUT_CREATE, + TABLE_LAYOUT_READ, + TABLE_METADATA_CREATE, + TABLE_OPTIONS_ALTER, + TABLE_OPTIONS_CREATE, + TABLE_OPTIONS_READ, + TRANSACTION_DDL, + VIEW_BARRIER_CREATE, + VIEW_BARRIER_READ, + CatalogCapabilities, +) from vgi.catalog.catalog_interface import ( AttachOpaqueData, CatalogAttachResult, + IndexInfo, + MacroInfo, OnConflict, + SchemaInfo, SchemaObjectType, SerializedSchema, SqlExpression, + TableInfo, TransactionOpaqueData, + ViewInfo, _validate_at_params, serialize_column_statistics, ) +from vgi.catalog.column_metadata import validate_column_metadata +from vgi.catalog.constraints import ( + ConstraintDefinition, + ConstraintEnforcement, + ConstraintsApplyRequest, + ConstraintSnapshot, + ForeignKeyMatch, + ReferentialAction, + constraint_definitions, + validate_constraint_schema, +) +from vgi.catalog.dependencies import CatalogObjectRef, DependencySnapshot +from vgi.catalog.indexes import IndexConstraintType, IndexDefinition +from vgi.catalog.layout import LayoutChangeMode, LayoutCoverage, TableLayout, TableLayoutChange +from vgi.catalog.metadata_changes import CatalogMetadataChanges +from vgi.catalog.options import OptionChanges, OptionDefinitions +from vgi.catalog.query import QUERY_EXECUTOR, CatalogQueryPrepareRequest, CatalogQueryPrepareResult from vgi.catalog.secret_type import SecretTypeSpec from vgi.catalog.setting import SettingSpec, extract_setting_specs +from vgi.catalog.sql import column_expressions +from vgi.column_path import column_definition as decode_column_definition +from vgi.column_path import column_path_key from vgi.exceptions import ( InvalidArgumentError, NotFoundError, @@ -118,6 +178,7 @@ class MyWorker(Worker): GlobalInitResponse, ) from vgi.logging_config import LogFormat, LogLevel +from vgi.logical_types import require_logical_profiles, require_logical_values from vgi.otel import VgiTracer, get_noop_tracer from vgi.protocol import ( BindRequest, @@ -145,6 +206,8 @@ class MyWorker(Worker): TableFunctionDynamicToStringRequest, TableFunctionDynamicToStringResponse, TableFunctionPlanRequest, + TableFunctionProjectionRequest, + TableFunctionProjectionResponse, TableFunctionStatisticsRequest, TableInOutExchangeState, TableProducerState, @@ -156,7 +219,7 @@ class MyWorker(Worker): ) from vgi.rpc_server import build_rpc_server from vgi.scalar_function import ScalarFunctionGenerator -from vgi.schema_path import schema_path_display, schema_path_key +from vgi.schema_path import schema_path_display, schema_path_key, validate_default_schema from vgi.table_buffering_function import ( TableBufferingFunction, TableBufferingParams, @@ -176,6 +239,7 @@ class MyWorker(Worker): from vgi.table_in_out_function import ( TableInOutGenerator, ) +from vgi.typed_values import TypedValueBatch if TYPE_CHECKING: from collections.abc import Callable @@ -1341,6 +1405,8 @@ def _build_registry(cls) -> dict[str, list[type[Function]]]: def add_function(func_cls: type[Function], schema_path: list[str] | None) -> None: meta = func_cls.get_metadata() + if meta.name.casefold() == QUERY_EXECUTOR: + raise ValueError(f"{QUERY_EXECUTOR} is reserved for the catalog query executor") if schema_path is not None: key = (schema_path_key(schema_path), meta.name) bucket = schema_registry.setdefault(key, []) @@ -1356,6 +1422,23 @@ def add_function(func_cls: type[Function], schema_path: list[str] | None) -> Non # them into, so a qualified call has to find them there. default_schema = catalog_obj.default_schema if catalog_obj is not None else "main" for func_cls in cls.functions: + if default_schema is None: + assert catalog_obj is not None + explicitly_registered = any( + func_cls in schema.functions + or any( + func_cls is getattr(table, attribute, None) + for table in schema.tables + for attribute in ("function", "insert_function", "update_function", "delete_function") + ) + for schema in catalog_obj.schemas + ) + if explicitly_registered: + continue + raise ValueError( + f"Function '{func_cls.get_metadata().name}' requires explicit schema registration " + "when catalog.default_schema is None" + ) add_function(func_cls, [default_schema]) # Declarative pattern: functions in catalog schemas @@ -1493,7 +1576,7 @@ def hosted_protocols(cls) -> Sequence[tuple[type, object]]: result is hosted on every transport this worker serves (stdin/stdout, AF_UNIX or named pipe, TCP, the Iroh raw upstream and HTTP), after the worker's own protocol and in the order returned, so reflection's - ``list_protocols`` reports ``vgi.v2`` first and then these. + ``list_protocols`` reports ``vgi.v3`` first and then these. Called **once**, when the server is built (see [`build_rpc_server`][vgi.rpc_server.build_rpc_server]). It may consult @@ -1513,7 +1596,7 @@ def hosted_protocols(cls) -> Sequence[tuple[type, object]]: error naming this method. Requests are routed on their ``vgi_rpc.protocol`` key, so hosting more - protocols never changes how a ``vgi.v2`` request is dispatched. + protocols never changes how a ``vgi.v3`` request is dispatched. Returns: The pairs to host; empty by default. @@ -2515,6 +2598,11 @@ def _resolve_function(self, request: BindRequest) -> type[Function]: ValueError: If function not found or ambiguous. """ + if request.function_name == QUERY_EXECUTOR: + from vgi.query_execution import _check_bind, executor_type + + _check_bind(request) + return executor_type(self) candidates = self._candidates_for(request.function_name, request.schema_path) if len(candidates) == 1: return candidates[0] @@ -2622,10 +2710,10 @@ def _seal_attach(self, plaintext: bytes) -> AttachOpaqueData: crypto.seal_bytes(plaintext, key, aad=_attach_aad(current_auth()), version=_ATTACH_ENVELOPE_VERSION) ) - def _unwrap_attach_full_with_auth(self, envelope: bytes | None, auth: AuthContext | None) -> bytes | None: - """Open an attach envelope under an explicit ``auth``, returning the full plaintext. + def _open_attach_context(self, envelope: bytes | None, auth: AuthContext | None) -> bytes | None: + """Authenticate and open an attachment, retaining its capability context. - The full framework plaintext is ``uuid(16) || catalog_bytes``. Pass-through + Strips only MetaWorker routing; the inner context stays cached. Pass-through when there is no signing key. Raises the uniform :meth:`_opaque_data_rejected` error on any open failure. @@ -2674,6 +2762,287 @@ def _unwrap_attach_full_with_auth(self, envelope: bytes | None, auth: AuthContex self._attach_unwrap_cache.put(cache_key, plaintext) return plaintext + def _unwrap_attach_full_with_auth(self, envelope: bytes | None, auth: AuthContext | None) -> bytes | None: + """Return UUID/provider bytes without routing or capability context.""" + plaintext = self._open_attach_context(envelope, auth) + if plaintext is None: + return None + try: + return attach_capabilities.split(plaintext)[1] + except ValueError as exc: + raise self._opaque_data_rejected("attach_opaque_data") from exc + + def _catalog_agreement(self, envelope: bytes) -> CatalogCapabilities: + """Read the immutable selected profiles from an authenticated attach.""" + plaintext = self._open_attach_context(envelope, current_auth()) + assert plaintext is not None + try: + return attach_capabilities.agreement(plaintext) + except (ValueError, TypeError, KeyError) as exc: + raise self._opaque_data_rejected("attach_opaque_data") from exc + + def _require_catalog_capability( + self, + envelope: bytes, + profile: str, + operation: str, + transaction: bytes | None = None, + ) -> None: + """Reject unsupported/stale operations before invoking provider effects.""" + available = self._get_catalog().catalog_capabilities(attach_opaque_data=self._unwrap_attach(envelope)) + selected = self._catalog_agreement(envelope) + selected.require(profile, available=available, operation=operation) + if transaction is not None and profile in CatalogCapabilities._known["ddl"]: + selected.require(TRANSACTION_DDL, available=available, operation=operation) + + def _effective_catalog_capabilities(self, envelope: bytes) -> CatalogCapabilities: + selected = self._catalog_agreement(envelope) + available = self._get_catalog().catalog_capabilities(attach_opaque_data=self._unwrap_attach(envelope)) + return available.select(selected) + + def _require_dependency_transaction(self, attach: bytes, transaction: bytes | None, operation: str) -> None: + """Keep dependency-aware metadata and DDL in the negotiated transaction.""" + if transaction is None: + return + selected = self._catalog_agreement(attach) + available = self._get_catalog().catalog_capabilities(attach_opaque_data=self._unwrap_attach(attach)) + # Omitting the graph read profile cannot weaken a provider's dependency + # mutation contract. Legacy providers without it retain their behavior. + if CATALOG_DEPENDENCIES_READ in selected.read or CATALOG_DEPENDENCIES_READ in available.read: + self._require_catalog_capability(attach, TRANSACTION_DDL, operation) + + @staticmethod + def _require_column_expression_profiles( + schema: pa.Schema, + selected: CatalogCapabilities, + operation: str, + owner_schema_path: list[str] | None = None, + ) -> bool: + """Validate canonical field expressions recursively without rewriting metadata. + + DEFAULT and generated definitions use the mandatory owned scalar + representation, including fields nested inside struct, dictionary or + extension storage types. Receiving providers qualify execution support. + """ + pending = list(schema) + found = False + while pending: + field = pending.pop() + if b"generated_expression" in (field.metadata or {}): + raise ValueError("generated columns require canonical SqlExpression metadata") + expressions = column_expressions(field) + if expressions.generated is not None and expressions.generation != "virtual": + raise NotImplementedError("stored generated columns are not supported") + for expression in (expressions.default, expressions.generated): + if expression is not None: + if owner_schema_path is not None: + expression.validate_owner(owner_schema_path) + if expression.profile != ROW_SCALAR_EXPRESSION: + raise ValueError("unsupported column expression profile") + found = True + dtype = field.type + while isinstance(dtype, pa.BaseExtensionType) or pa.types.is_dictionary(dtype): + dtype = dtype.storage_type if isinstance(dtype, pa.BaseExtensionType) else dtype.value_type + pending.extend(dtype.field(index) for index in range(dtype.num_fields)) + return found + + def _validate_column_definition_expressions( + self, + attach: bytes, + transaction: bytes | None, + schema: pa.Schema, + owner_schema_path: list[str], + operation: str, + ) -> None: + """Validate CREATE/ADD expression metadata before provider effects.""" + validate_column_metadata(schema) + found = self._require_column_expression_profiles( + schema, self._effective_catalog_capabilities(attach), operation, owner_schema_path + ) + pending = list(schema) + while pending: + field = pending.pop() + if {b"default", b"generated_expression"}.intersection(field.metadata or {}): + raise ValueError("column definitions require canonical SqlExpression metadata, not legacy SQL text") + dtype = field.type + while isinstance(dtype, pa.BaseExtensionType) or pa.types.is_dictionary(dtype): + dtype = dtype.storage_type if isinstance(dtype, pa.BaseExtensionType) else dtype.value_type + pending.extend(dtype.field(index) for index in range(dtype.num_fields)) + if found and transaction is not None: + self._require_catalog_capability(attach, TRANSACTION_DDL, operation) + + @staticmethod + def _column_expression(payload: bytes, owner_schema_path: list[str]) -> SqlExpression: + """Decode the mandatory owned scalar representation without negotiation.""" + expression = SqlExpression.deserialize_from_bytes(payload) + expression.validate_owner(owner_schema_path) + if expression.profile != ROW_SCALAR_EXPRESSION: + raise ValueError("unsupported column expression profile") + return expression + + @staticmethod + def _schema_for_capabilities(info: SchemaInfo, selected: CatalogCapabilities) -> SchemaInfo: + if SCHEMA_OPTIONS_READ not in selected.read and info.options is not None: + return _dataclass_replace(info, options=None) + return info + + def _check_existing_view_security( + self, + attach: bytes, + transaction: bytes | None, + path: list[str], + name: str, + *, + replace: bool = False, + ) -> None: + """An unaware caller cannot replace a barrier with an ordinary view.""" + info = self._get_catalog().view_get( + attach_opaque_data=self._unwrap_attach(attach), + transaction_opaque_data=self._unwrap_tx_opt(transaction, attach), + schema_path=path, + name=name, + ) + if info is not None: + self._view_for_capabilities(info, self._effective_catalog_capabilities(attach)) + if transaction is not None and info.security == "barrier": + self._require_catalog_capability(attach, TRANSACTION_DDL, "mutate barrier view") + if replace and info.security == "barrier": + self._require_catalog_capability(attach, VIEW_BARRIER_CREATE, "replace barrier view", transaction) + + @staticmethod + def _view_for_capabilities(info: ViewInfo, selected: CatalogCapabilities) -> ViewInfo: + """Require barrier support even for discovery; never strip security metadata.""" + ViewInfo.validate_security(info.security) + if info.security == "barrier": + selected.require(VIEW_BARRIER_READ, available=selected, operation="read barrier view") + return info + + @staticmethod + def _macro_for_capabilities(info: MacroInfo, selected: CatalogCapabilities) -> MacroInfo: + """Validate mandatory macro families and their independently negotiated logical types.""" + info.__post_init__() + for member in info.overloads: + for parameter in member.parameters: + if parameter.declared_type is not None: + require_logical_profiles(parameter.declared_type, selected_profiles=selected.read) + if parameter.default_value is not None: + require_logical_values(parameter.default_value, selected_profiles=selected.read) + return info + + @staticmethod + def _index_for_capabilities(info: IndexInfo, selected: CatalogCapabilities) -> IndexInfo: + """Never expose typed definitions without the selected read profile.""" + info.__post_init__() + selected.require(INDEX_DEFINITION_READ, available=selected, operation="read typed index") + definition = info.definition + require_logical_values(definition.options, selected_profiles=selected.read) + for key in definition.key_definitions(): + if key.expression is not None: + selected.require(key.expression.profile, available=selected, operation="read index expression") + if definition.associated_constraint is not None: + selected.require(CONSTRAINT_RECORDS_READ, available=selected, operation="read associated index") + return info + + def _require_index_definition_profiles(self, attach: bytes, definition: IndexDefinition) -> None: + """Gate expressions and enforcement before provider index construction.""" + definition.__post_init__() + selected = self._effective_catalog_capabilities(attach) + require_logical_values(definition.options, selected_profiles=selected.read) + for key in definition.key_definitions(): + if key.expression is not None: + self._require_catalog_capability(attach, key.expression.profile, "create index expression") + if definition.constraint_type != IndexConstraintType.NONE: + self._require_catalog_capability(attach, CONSTRAINT_ENFORCE, "create enforcing index") + if definition.associated_constraint is not None: + self._require_catalog_capability(attach, CONSTRAINT_MUTATE, "associate index with constraint") + + @staticmethod + def _table_for_capabilities(info: TableInfo, selected: CatalogCapabilities) -> TableInfo: + schema = pa.ipc.read_schema(pa.py_buffer(info.columns)) + validate_column_metadata(schema) + require_logical_profiles(schema, selected_profiles=selected.read) + Worker._require_column_expression_profiles(schema, selected, "read column expressions", info.schema_path) + infos = info.constraint_infos() + validate_constraint_schema( + schema, [item.definition for item in infos], trusted=[item.definition for item in infos if item.trusted] + ) + if infos: + selected.require(CONSTRAINT_RECORDS_READ, available=selected, operation="read canonical constraints") + for item in infos: + if item.definition.check_expression is not None: + selected.require( + item.definition.check_expression.profile, available=selected, operation="read CHECK constraint" + ) + if CONSTRAINT_MUTATE in selected.ddl and not info.definition_revision: + raise ValueError("canonical mutation requires a nonempty table definition revision") + write_allowed = CONSTRAINT_ENFORCE in selected.write or not any( + item.enforcement == ConstraintEnforcement.ENFORCED for item in infos + ) + return _dataclass_replace( + info, + write_result_modes=info.write_result_modes if write_allowed else {}, + insert_function=info.insert_function if write_allowed else None, + update_function=info.update_function if write_allowed else None, + delete_function=info.delete_function if write_allowed else None, + options=info.options if TABLE_OPTIONS_READ in selected.read else None, + layout=info.layout + if ( + TABLE_LAYOUT_READ in selected.read + and info.layout is not None + and all( + expr.profile in selected.read + for expr in ( + info.layout.partition_expressions() + [key.expression for key in info.layout.sort_keys()] + ) + ) + ) + else None, + ) + + def _require_constraint_definition_profiles( + self, attach: bytes, definitions: list[ConstraintDefinition], operation: str + ) -> None: + """Reject unqualified expression and propagating FK semantics before effects.""" + for definition in definitions: + if definition.check_expression is not None: + self._require_catalog_capability(attach, definition.check_expression.profile, operation) + foreign_key = definition.foreign_key + if foreign_key is not None and ( + foreign_key.match != ForeignKeyMatch.SIMPLE + or foreign_key.on_update not in {ReferentialAction.NO_ACTION, ReferentialAction.RESTRICT} + or foreign_key.on_delete not in {ReferentialAction.NO_ACTION, ReferentialAction.RESTRICT} + ): + raise NotImplementedError("additional foreign-key match/action profiles are not implemented") + + def _check_table_constraint_write( + self, attach: bytes, transaction: bytes | None, path: list[str], name: str + ) -> None: + """Require canonical discovery and enforcement before exposing write endpoints.""" + info = self._get_catalog().table_get( + attach_opaque_data=self._unwrap_attach(attach), + transaction_opaque_data=self._unwrap_tx_opt(transaction, attach), + schema_path=path, + name=name, + ) + if info is not None: + self._table_for_capabilities(info, self._effective_catalog_capabilities(attach)) + if any(item.enforcement == ConstraintEnforcement.ENFORCED for item in info.constraint_infos()): + self._require_catalog_capability(attach, CONSTRAINT_ENFORCE, "write constrained table") + + def _check_table_constraint_mutation( + self, attach: bytes, transaction: bytes | None, path: list[str], name: str + ) -> None: + """Legacy structural DDL must preserve canonical identities and revisions.""" + info = self._get_catalog().table_get( + attach_opaque_data=self._unwrap_attach(attach), + transaction_opaque_data=self._unwrap_tx_opt(transaction, attach), + schema_path=path, + name=name, + ) + if info is not None and info.constraints: + self._table_for_capabilities(info, self._effective_catalog_capabilities(attach)) + self._require_catalog_capability(attach, CONSTRAINT_MUTATE, "mutate constrained table", transaction) + def _attach_catalog_name(self, envelope: bytes | None) -> str | None: """Read the catalog name MetaWorker sealed into an attach envelope. @@ -2700,7 +3069,7 @@ def _seal_attach_with_catalog(self, envelope: bytes, catalog_name: str) -> Attac seals again. Used by [`MetaWorker`][] at ``catalog_attach`` — the one place that knows both the catalog name and the freshly minted attach. """ - plaintext = self._unwrap_attach_full_with_auth(envelope, current_auth()) + plaintext = self._open_attach_context(envelope, current_auth()) assert plaintext is not None # a non-None envelope always yields plaintext return self._seal_attach(attach_header.encode(catalog_name, plaintext)) @@ -2817,6 +3186,13 @@ def _unwrap_tx_opt(self, envelope: bytes | None, attach_envelope: bytes) -> Tran """ return self._unwrap_transaction(envelope, attach_envelope) if envelope else None + def _unwrap_transaction_for(self, request: Any) -> TransactionOpaqueData | None: + """Open a function request's transaction without changing its sealed wire state.""" + bind = getattr(request, "bind_call", request) + return self._unwrap_tx_opt( + getattr(bind, "transaction_opaque_data", None), getattr(bind, "attach_opaque_data", None) or b"" + ) + def _get_catalog(self) -> CatalogInterface: """Get the [`CatalogInterface`][] instance for this worker. @@ -2880,7 +3256,44 @@ def bind(self, request: BindRequest, ctx: CallContext) -> BindResponse: self._validate_required_settings(func_cls, request) self._validate_bind_shape(func_cls, request) instance = func_cls(logger=_logger) - return instance.bind(request, ctx=ctx, attach_plaintext=attach_plaintext) # type: ignore[attr-defined, no-any-return] + if isinstance(instance, TableFunctionBase): + result = instance.bind( + request, + ctx=ctx, + attach_plaintext=attach_plaintext, + transaction_plaintext=self._unwrap_transaction_for(request), + ) + else: + result = instance.bind(request, ctx=ctx, attach_plaintext=attach_plaintext) # type: ignore[attr-defined] + if not isinstance(result, BindResponse): + raise TypeError("bind must return BindResponse") + result.__post_init__() + return result + + def table_function_projection( + self, request: TableFunctionProjectionRequest, ctx: CallContext + ) -> TableFunctionProjectionResponse: + """Validate a pure optional projection proposal before any plan rewrite.""" + request.__post_init__() + attach_plaintext = self._unwrap_attach_full(request.bind_call.attach_opaque_data) + func_cls = self._resolve_function(request.bind_call) + self._validate_required_settings(func_cls, request.bind_call) + self._validate_bind_shape(func_cls, request.bind_call) + if not issubclass(func_cls, TableFunctionGenerator): + raise ValueError("scalar projection only applies to ordinary source table functions") + params = func_cls._make_bind_params( + request.bind_call, + auth_context=ctx.auth, + attach_plaintext=attach_plaintext, + transaction_plaintext=self._unwrap_transaction_for(request), + ) + result = func_cls.on_projection(params, request) + if params.secrets.needs_resolution: + raise ValueError("scalar projection cannot initiate secret resolution") + if not isinstance(result, TableFunctionProjectionResponse): + raise TypeError("on_projection must return TableFunctionProjectionResponse") + result.validate_request(request) + return result def _validate_bind_shape(self, func_cls: type[Function], request: BindRequest) -> None: """Reject a bind() call whose input_schema doesn't match the function's shape. @@ -2946,7 +3359,12 @@ def table_function_cardinality( f" functions, but '{func_cls.__name__}' is not a TableFunctionGenerator." ) return func_cls.cardinality( - func_cls._make_bind_params(request.bind_call, auth_context=ctx.auth, attach_plaintext=attach_plaintext) + func_cls._make_bind_params( + request.bind_call, + auth_context=ctx.auth, + attach_plaintext=attach_plaintext, + transaction_plaintext=self._unwrap_transaction_for(request), + ) ) def _current_split_anchor(self, request: Any, ctx: CallContext | None) -> bytes | None: @@ -3099,7 +3517,12 @@ def table_function_plan(self, request: TableFunctionPlanRequest, ctx: CallContex "scan planning only applies to table functions." ) raise TypeError(msg) - params = func_cls._make_bind_params(request.bind_call, auth_context=ctx.auth, attach_plaintext=attach_plaintext) + params = func_cls._make_bind_params( + request.bind_call, + auth_context=ctx.auth, + attach_plaintext=attach_plaintext, + transaction_plaintext=self._unwrap_transaction_for(request), + ) response = on_plan(params, request) if isinstance(response, PlanResponse): response = self._stamp_split_tokens(response, request, ctx) @@ -3119,7 +3542,12 @@ def table_function_statistics(self, request: TableFunctionStatisticsRequest, ctx if not issubclass(func_cls, TableFunctionGenerator): return None stats = func_cls.statistics( - func_cls._make_bind_params(request.bind_call, auth_context=ctx.auth, attach_plaintext=attach_plaintext) + func_cls._make_bind_params( + request.bind_call, + auth_context=ctx.auth, + attach_plaintext=attach_plaintext, + transaction_plaintext=self._unwrap_transaction_for(request), + ) ) if not stats: return None @@ -3150,6 +3578,7 @@ def table_function_dynamic_to_string( auth_context=ctx.auth, execution_id=request.global_execution_id, attach_plaintext=attach_plaintext, + transaction_plaintext=self._unwrap_transaction_for(request), ) mapping = func_cls.dynamic_to_string(params, request.global_execution_id) except Exception: @@ -3618,6 +4047,7 @@ def _load_table_buffering_params( storage=cold_storage, auth_context=ctx.auth, attach_opaque_data=catalog_bytes, + transaction_opaque_data=self._unwrap_transaction_for(init_call), execution_id=request.execution_id, attach_id=attach_id, transaction_id=transaction_id, @@ -4271,8 +4701,15 @@ def init(self, request: InitRequest, ctx: CallContext) -> Stream[ProcessState, G # off the wire, because serialized state reaches the access log in # plaintext — see VgiCallState in vgi/protocol.py. attach_plaintext = self._unwrap_attach_full(getattr(request.bind_call, "attach_opaque_data", None)) + if request.bind_call.function_name == QUERY_EXECUTOR: + from vgi.query_execution import check_init + + check_init(request) func_cls = self._resolve_function(request.bind_call) instance = func_cls(logger=_logger) + transaction_plaintext = ( + self._unwrap_transaction_for(request) if isinstance(instance, TableFunctionBase) else None + ) # A SPLIT init looks secondary — it joins an execution the planner already # minted — but it MUST run user code, because a split *names* work and only @@ -4292,6 +4729,7 @@ def init(self, request: InitRequest, ctx: CallContext) -> Stream[ProcessState, G request, ctx=ctx, attach_plaintext=attach_plaintext, + transaction_plaintext=transaction_plaintext, signing_key=self._signing_key, current_anchor=(self._current_split_anchor(request, ctx) if request.split_tokens else None), ) @@ -4369,6 +4807,7 @@ def init(self, request: InitRequest, ctx: CallContext) -> Stream[ProcessState, G storage=cold_storage, auth_context=ctx.auth, attach_opaque_data=attach_catalog_bytes(attach_plaintext), + transaction_opaque_data=transaction_plaintext, execution_id=init_response.execution_id, attach_id=attach_id, transaction_id=getattr( @@ -4443,6 +4882,7 @@ def init(self, request: InitRequest, ctx: CallContext) -> Stream[ProcessState, G ), auth_context=ctx.auth, attach_opaque_data=attach_catalog_bytes(attach_plaintext), + transaction_opaque_data=transaction_plaintext, ) if request.pushdown_filters is not None: current_filters = type(instance).pushdown_filters( @@ -4543,6 +4983,7 @@ def init(self, request: InitRequest, ctx: CallContext) -> Stream[ProcessState, G ), auth_context=ctx.auth, attach_opaque_data=attach_catalog_bytes(attach_plaintext), + transaction_opaque_data=transaction_plaintext, # The key travels with the params because split_payloads is # resolved lazily, per split, long after this frame. signing_key=self._signing_key, @@ -4663,17 +5104,48 @@ def catalog_attach( options=options, data_version_spec=request.data_version_spec, implementation_version=request.implementation_version, + client_capabilities=request.client_capabilities, ctx=ctx, ) + validate_default_schema(result.default_schema) + available = cat.catalog_capabilities(attach_opaque_data=result.attach_opaque_data) + offered = request.client_capabilities.catalog if request.client_capabilities is not None else None + selected = available.select(offered) + if TRANSACTION_DDL in selected.transaction and not result.supports_transactions: + raise ValueError("DDL transaction profile requires supports_transactions") + from vgi.query_execution import negotiate + + minted = ( + uuid.uuid4().bytes + bytes(result.attach_opaque_data) if result.attach_opaque_data is not None else None + ) + query_offer = request.client_capabilities.query_pushdown if request.client_capabilities is not None else None + query_selected = ( + negotiate(self, minted, query_offer, supports_transactions=result.supports_transactions) + if minted is not None + else None + ) + result = _dataclass_replace( + result, + catalog_capabilities=available, + selected_capabilities=selected, + query_pushdown=query_selected, + attach_opaque_data_required=result.attach_opaque_data_required + or bool(selected.names()) + or query_selected is not None, + ) # Mint the shard identity: prepend a fresh framework UUID to the # catalog's plaintext (``uuid(16) || catalog_bytes``), then seal. Storage # shards on this UUID — stable across re-seals and globally unique, unlike # the random-nonce ciphertext or the (possibly non-unique) catalog bytes. # ``_unwrap_attach`` strips the UUID back off, so the catalog only ever # sees its own bytes. See ``_AttachUnwrapper``. - if result.attach_opaque_data is not None: - minted = uuid.uuid4().bytes + bytes(result.attach_opaque_data) - result = _dataclass_replace(result, attach_opaque_data=self._seal_attach(minted)) + if minted is not None: + result = _dataclass_replace( + result, + attach_opaque_data=self._seal_attach( + attach_capabilities.encode(minted, selected, query_offer=query_offer, query_selected=query_selected) + ), + ) loggable = dict(cat.loggable_attach_options(options)) self._log_catalog_lifecycle( "catalog.attach", @@ -4815,8 +5287,9 @@ def catalog_schemas( for path_key in path_keys: if len(path_key) > 1 and path_key[:-1] not in path_key_set: raise ValueError(f"Catalog returned schema path {list(path_key)!r} without its parent") + selected = self._effective_catalog_capabilities(attach_opaque_data) ordered = sorted(info_list, key=lambda info: len(info.path)) - return SchemasResponse.from_infos(ordered) + return SchemasResponse.from_infos([self._schema_for_capabilities(info, selected) for info in ordered]) def catalog_schema_get( self, attach_opaque_data: bytes, path: list[str], transaction_opaque_data: bytes | None = None @@ -4829,7 +5302,10 @@ def catalog_schema_get( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), path=path, ) - return SchemasResponse.from_optional(info) + selected = self._effective_catalog_capabilities(attach_opaque_data) + return SchemasResponse.from_optional( + self._schema_for_capabilities(info, selected) if info is not None else None + ) def catalog_schema_create( self, @@ -4839,10 +5315,23 @@ def catalog_schema_create( comment: str | None = None, tags: dict[str, str] | None = None, transaction_opaque_data: bytes | None = None, + options: bytes | None = None, ) -> None: """Create a new schema.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "create schema") + schema_path_key(path) + if any("\0" in component for component in path): + raise ValueError("schema target components must contain no NUL") + if SCHEMA_LIFECYCLE in self._catalog_agreement(attach_opaque_data).ddl: + self._require_catalog_capability( + attach_opaque_data, SCHEMA_LIFECYCLE, "catalog_schema_create", transaction_opaque_data + ) self._enrich_catalog_span(vgi_schema_path=path) cat = self._get_catalog() + if options is not None: + self._require_catalog_capability( + attach_opaque_data, SCHEMA_OPTIONS_CREATE, "catalog_schema_create", transaction_opaque_data + ) cat.schema_create( attach_opaque_data=self._unwrap_attach(attach_opaque_data), transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), @@ -4850,8 +5339,91 @@ def catalog_schema_create( on_conflict=on_conflict, comment=comment, tags=tags or {}, + options=TypedValueBatch.deserialize_from_bytes(options) if options is not None else None, ) + def catalog_schema_option_definitions( + self, + attach_opaque_data: bytes, + path: list[str], + transaction_opaque_data: bytes | None = None, + ) -> OptionDefinitions: + """Return definitions only under a current, selected read profile.""" + self._require_catalog_capability(attach_opaque_data, SCHEMA_OPTIONS_READ, "catalog_schema_option_definitions") + return self._get_catalog().schema_option_definitions( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + path=path, + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + ) + + def catalog_schema_alter_metadata( + self, + attach_opaque_data: bytes, + path: list[str], + changes: bytes, + ignore_not_found: bool = False, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Validate the full metadata patch before invoking the provider.""" + self._require_catalog_capability( + attach_opaque_data, SCHEMA_METADATA_ALTER, "catalog_schema_alter_metadata", transaction_opaque_data + ) + schema_path_key(path) + if any("\0" in component for component in path): + raise ValueError("schema metadata target components must contain no NUL") + patch = CatalogMetadataChanges.deserialize_from_bytes(changes) + self._get_catalog().schema_alter_metadata( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + path=path, + changes=patch, + ignore_not_found=ignore_not_found, + ) + + def catalog_schema_alter_options( + self, + attach_opaque_data: bytes, + path: list[str], + changes: bytes, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Apply the complete change under selected DDL/transaction profiles.""" + self._require_catalog_capability( + attach_opaque_data, SCHEMA_OPTIONS_ALTER, "catalog_schema_alter_options", transaction_opaque_data + ) + self._get_catalog().schema_alter_options( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + path=path, + changes=OptionChanges.deserialize_from_bytes(changes), + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + ) + + def catalog_query_prepare(self, request: CatalogQueryPrepareRequest) -> CatalogQueryPrepareResult: + """Prepare under the authenticated attachment's immutable query agreement.""" + from vgi.query_execution import prepare + + return prepare(self, request) + + def catalog_query_release( + self, attach_opaque_data: bytes, prepared_query_token: bytes, transaction_opaque_data: bytes | None = None + ) -> None: + """Release this owner's receipt, including after expiry or capability withdrawal.""" + from vgi.query_execution import release + + release(self, attach_opaque_data, prepared_query_token, transaction_opaque_data) + + def catalog_dependencies( + self, attach_opaque_data: bytes, transaction_opaque_data: bytes | None = None + ) -> DependencySnapshot: + """Read and validate a complete graph from one provider snapshot.""" + self._require_catalog_capability(attach_opaque_data, CATALOG_DEPENDENCIES_READ, "catalog_dependencies") + result = self._get_catalog().catalog_dependencies( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + ) + result.__post_init__() + return result + def catalog_schema_drop( self, attach_opaque_data: bytes, @@ -4861,6 +5433,14 @@ def catalog_schema_drop( transaction_opaque_data: bytes | None = None, ) -> None: """Drop a schema.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "drop schema") + schema_path_key(path) + if any("\0" in component for component in path): + raise ValueError("schema target components must contain no NUL") + if SCHEMA_LIFECYCLE in self._catalog_agreement(attach_opaque_data).ddl: + self._require_catalog_capability( + attach_opaque_data, SCHEMA_LIFECYCLE, "catalog_schema_drop", transaction_opaque_data + ) self._enrich_catalog_span(vgi_schema_path=path) cat = self._get_catalog() cat.schema_drop( @@ -4886,7 +5466,8 @@ def catalog_schema_contents_tables( path=path, type=SchemaObjectType.TABLE, ) - return TablesResponse.from_infos(list(infos)) + selected = self._effective_catalog_capabilities(attach_opaque_data) + return TablesResponse.from_infos([self._table_for_capabilities(info, selected) for info in infos]) def catalog_schema_contents_views( self, @@ -4903,7 +5484,8 @@ def catalog_schema_contents_views( path=path, type=SchemaObjectType.VIEW, ) - return ViewsResponse.from_infos(list(infos)) + selected = self._effective_catalog_capabilities(attach_opaque_data) + return ViewsResponse.from_infos([self._view_for_capabilities(info, selected) for info in infos]) def catalog_schema_contents_functions( self, @@ -4941,6 +5523,21 @@ def catalog_contents(self, attach_opaque_data: bytes, if_none_match: str | None # Open the envelope first even on a cache hit: it is the auth check. attach = self._unwrap_attach(attach_opaque_data) version = cat.catalog_version(attach_opaque_data=attach, transaction_opaque_data=None) + selected = self._catalog_agreement(attach_opaque_data) + available = cat.catalog_capabilities(attach_opaque_data=attach) + effective = available.select(selected) + projected = bool(selected.names() or available.names()) + projection_key = hashlib.sha256(effective.serialize_to_bytes()).hexdigest() if projected else "" + + def build_response() -> CatalogContentsResponse: + response = self._catalog_contents_response(cat, attach, version, None, effective) + if projected: + etag = hashlib.sha256( + effective.serialize_to_bytes() + catalog_contents_digest(response).encode() + ).hexdigest() + response = _dataclass_replace(response, etag=etag) + return response + policy = cat.catalog_snapshot_policy if policy is not None and policy.delivery == "published": from vgi.catalog.contents_codec import decode_inventory @@ -4952,13 +5549,14 @@ def catalog_contents(self, attach_opaque_data: bytes, if_none_match: str | None attach_opaque_data=attach, attachment_scope=hashlib.sha256(attach_opaque_data).hexdigest(), catalog_version=version, + capability_projection=projection_key, ) built: CatalogContentsResponse | None = None def build() -> CatalogContentsResponse: nonlocal built if built is None: - candidate = self._catalog_contents_response(cat, attach, version, None) + candidate = build_response() decode_inventory(candidate) after_version = cat.catalog_version(attach_opaque_data=attach, transaction_opaque_data=None) if after_version != version: @@ -4979,6 +5577,11 @@ def build() -> CatalogContentsResponse: if response.etag is not None and response.etag == if_none_match: return CatalogContentsResponse(catalog_version=version, etag=response.etag, not_modified=True) return response + if projected: + response = build_response() + if response.etag == if_none_match: + return CatalogContentsResponse(catalog_version=version, etag=response.etag, not_modified=True) + return response if type(cat).catalog_contents_attach_independent and getattr(type(cat), "catalog_version_frozen", False): snapshot = self._contents_cache if snapshot is None or snapshot.catalog is not cat or snapshot.catalog_version != version: @@ -5026,11 +5629,18 @@ def import_catalog_publication(self, attach_opaque_data: bytes, manifest: Any) - if policy is None or policy.delivery != "published": raise ValueError("catalog has no published snapshot policy") version = cat.catalog_version(attach_opaque_data=attach, transaction_opaque_data=None) + selected = self._catalog_agreement(attach_opaque_data) + available = cat.catalog_capabilities(attach_opaque_data=attach) + effective = available.select(selected) + projection_key = ( + hashlib.sha256(effective.serialize_to_bytes()).hexdigest() if selected.names() or available.names() else "" + ) request = SnapshotRequest( catalog=cat, attach_opaque_data=attach, attachment_scope=hashlib.sha256(attach_opaque_data).hexdigest(), catalog_version=version, + capability_projection=projection_key, ) return self._catalog_snapshot_manager(policy).import_publication(request, manifest) @@ -5044,11 +5654,18 @@ def invalidate_catalog_publication(self, attach_opaque_data: bytes, manifest_id: if policy is None or policy.delivery != "published": raise ValueError("catalog has no published snapshot policy") version = cat.catalog_version(attach_opaque_data=attach, transaction_opaque_data=None) + selected = self._catalog_agreement(attach_opaque_data) + available = cat.catalog_capabilities(attach_opaque_data=attach) + effective = available.select(selected) + projection_key = ( + hashlib.sha256(effective.serialize_to_bytes()).hexdigest() if selected.names() or available.names() else "" + ) request = SnapshotRequest( catalog=cat, attach_opaque_data=attach, attachment_scope=hashlib.sha256(attach_opaque_data).hexdigest(), catalog_version=version, + capability_projection=projection_key, ) manager = self._catalog_snapshot_manager(policy) return bool(manager.invalidate(manager.cache_key(request), manifest_id)) @@ -5061,6 +5678,7 @@ def _catalog_contents_response( attach: AttachOpaqueData, version: int, if_none_match: str | None, + selected: CatalogCapabilities | None = None, ) -> CatalogContentsResponse: """Ask the catalog for its contents and shape the typed wire response. @@ -5082,7 +5700,23 @@ def _catalog_contents_response( from vgi.catalog.contents_codec import contents_digest, encode_inventory - categories = encode_inventory(list(result.schemas)) + infos = [ + _dataclass_replace( + item, + schema=Worker._schema_for_capabilities(item.schema, selected or CatalogCapabilities()), + tables=[Worker._table_for_capabilities(t, selected or CatalogCapabilities()) for t in item.tables], + views=[Worker._view_for_capabilities(v, selected or CatalogCapabilities()) for v in item.views], + scalar_macros=[ + Worker._macro_for_capabilities(m, selected or CatalogCapabilities()) for m in item.scalar_macros + ], + table_macros=[ + Worker._macro_for_capabilities(m, selected or CatalogCapabilities()) for m in item.table_macros + ], + indexes=[Worker._index_for_capabilities(ix, selected or CatalogCapabilities()) for ix in item.indexes], + ) + for item in result.schemas + ] + categories = encode_inventory(infos) etag = result.etag if etag is None and type(cat).catalog_contents_etag == "content-hash": etag = contents_digest(categories) @@ -5137,12 +5771,70 @@ def catalog_table_get( at_unit=at_unit, at_value=at_value, ) - return TablesResponse.from_optional(info) + return TablesResponse.from_optional( + self._table_for_capabilities(info, self._effective_catalog_capabilities(attach_opaque_data)) + if info is not None + else None + ) def catalog_table_create(self, request: TableCreateRequest) -> None: """Create a new table.""" + request.__post_init__() + self._require_dependency_transaction( + request.attach_opaque_data, request.transaction_opaque_data, "create table" + ) self._enrich_catalog_span(vgi_schema_path=request.schema_path, vgi_table_name=request.name) cat = self._get_catalog() + if request.on_conflict == OnConflict.REPLACE: + self._check_table_constraint_mutation( + request.attach_opaque_data, request.transaction_opaque_data, request.schema_path, request.name + ) + schema = pa.ipc.read_schema(pa.py_buffer(request.columns)) + self._validate_column_definition_expressions( + request.attach_opaque_data, + request.transaction_opaque_data, + schema, + request.schema_path, + "create column expressions", + ) + definitions = constraint_definitions( + request.constraint_definitions, + CatalogObjectRef(kind="table", schema_path=request.schema_path, name=request.name), + ) + validate_constraint_schema(schema, definitions) + if definitions: + self._require_catalog_capability( + request.attach_opaque_data, CONSTRAINT_CREATE, "catalog_table_create", request.transaction_opaque_data + ) + self._require_catalog_capability(request.attach_opaque_data, CONSTRAINT_ENFORCE, "catalog_table_create") + self._require_constraint_definition_profiles( + request.attach_opaque_data, definitions, "catalog_table_create" + ) + require_logical_profiles( + schema, + selected_profiles=self._effective_catalog_capabilities(request.attach_opaque_data).read, + ) + extra: dict[str, Any] = {} + for key, profile in ( + ("options", TABLE_OPTIONS_CREATE), + ("layout", TABLE_LAYOUT_CREATE), + ("comment", TABLE_METADATA_CREATE), + ("tags", TABLE_METADATA_CREATE), + ): + value = getattr(request, key) + if value is not None: + self._require_catalog_capability( + request.attach_opaque_data, profile, "catalog_table_create", request.transaction_opaque_data + ) + extra[key] = value + if request.layout is not None: + request.layout.__post_init__() + for expression in request.layout.partition_expressions() + [ + key.expression for key in request.layout.sort_keys() + ]: + self._require_catalog_capability(request.attach_opaque_data, expression.profile, "catalog_table_create") + if request.layout.coverage != LayoutCoverage.ALL_ROWS: + raise ValueError("new table layout must cover all rows") cat.table_create( attach_opaque_data=self._unwrap_attach(request.attach_opaque_data), transaction_opaque_data=self._unwrap_transaction( @@ -5154,17 +5846,109 @@ def catalog_table_create(self, request: TableCreateRequest) -> None: name=request.name, columns=SerializedSchema(request.columns), on_conflict=request.on_conflict, - not_null_constraints=list(request.not_null_constraints), - unique_constraints=[list(c) for c in request.unique_constraints], - check_constraints=list(request.check_constraints), - primary_key_constraints=( - [list(c) for c in request.primary_key_constraints] if request.primary_key_constraints else None - ), - foreign_key_constraints=( - list(request.foreign_key_constraints) if request.foreign_key_constraints else None - ), + constraint_definitions=list(request.constraint_definitions), + **extra, ) + def catalog_table_constraints_apply(self, request: ConstraintsApplyRequest) -> ConstraintSnapshot: + """Validate the complete atomic request before invoking the provider once.""" + request.__post_init__() + self._require_catalog_capability( + request.attach_opaque_data, + CONSTRAINT_MUTATE, + "catalog_table_constraints_apply", + request.transaction_opaque_data, + ) + changes = request.constraint_changes() + definitions = [change.definition for change in changes if change.definition is not None] + self._require_constraint_definition_profiles( + request.attach_opaque_data, definitions, "catalog_table_constraints_apply" + ) + if any(change.enforcement == ConstraintEnforcement.ENFORCED for change in changes): + self._require_catalog_capability( + request.attach_opaque_data, CONSTRAINT_ENFORCE, "catalog_table_constraints_apply" + ) + result = self._get_catalog().table_constraints_apply( + _dataclass_replace( + request, + attach_opaque_data=self._unwrap_attach(request.attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt( + request.transaction_opaque_data, request.attach_opaque_data + ), + ) + ) + result.__post_init__() + if result.table != request.table: + raise ValueError("constraint mutation returned a different table") + return result + + def catalog_table_option_definitions( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + transaction_opaque_data: bytes | None = None, + ) -> OptionDefinitions: + """Return current provider definitions under the table-option read profile.""" + self._require_catalog_capability(attach_opaque_data, TABLE_OPTIONS_READ, "catalog_table_option_definitions") + return self._get_catalog().table_option_definitions( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + schema_path=schema_path, + name=name, + ) + + def catalog_table_alter_options( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + changes: bytes, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Check capabilities before applying the complete table-option change.""" + self._require_catalog_capability( + attach_opaque_data, TABLE_OPTIONS_ALTER, "catalog_table_alter_options", transaction_opaque_data + ) + self._get_catalog().table_alter_options( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + schema_path=schema_path, + name=name, + changes=OptionChanges.deserialize_from_bytes(changes), + ) + + def catalog_table_alter_layout( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + changes: bytes, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Require the selected mode before asking a provider to alter its layout.""" + value = TableLayoutChange.deserialize_from_bytes(changes) + profile = TABLE_LAYOUT_ALTER_REWRITE if value.mode == LayoutChangeMode.REWRITE else TABLE_LAYOUT_ALTER_FUTURE + self._require_catalog_capability( + attach_opaque_data, profile, "catalog_table_alter_layout", transaction_opaque_data + ) + catalog = self._get_catalog() + args: dict[str, Any] = dict( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + schema_path=schema_path, + name=name, + ) + current = catalog.table_get(**args) + candidate = value.apply_to( + current.layout if current is not None and current.layout is not None else TableLayout() + ) + for expression in candidate.partition_expressions() + [key.expression for key in candidate.sort_keys()]: + self._require_catalog_capability(attach_opaque_data, expression.profile, "catalog_table_alter_layout") + # The provider revalidates the candidate in its mutation snapshot, since + # another request may change the table after this capability check. + catalog.table_alter_layout(**args, changes=value) + def catalog_table_drop( self, attach_opaque_data: bytes, @@ -5175,6 +5959,8 @@ def catalog_table_drop( transaction_opaque_data: bytes | None = None, ) -> None: """Drop a table.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "drop table") + self._check_table_constraint_mutation(attach_opaque_data, transaction_opaque_data, schema_path, name) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_drop( @@ -5268,6 +6054,7 @@ def catalog_table_insert_function_get( writable_branch_function_name: str | None = None, ) -> bytes: """Get the insert function for a table. Returns `WriteFunctionResult` as IPC bytes.""" + self._check_table_constraint_write(attach_opaque_data, transaction_opaque_data, schema_path, name) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() result = cat.table_insert_function_get( @@ -5287,6 +6074,7 @@ def catalog_table_update_function_get( transaction_opaque_data: bytes | None = None, ) -> bytes: """Get the update function for a table. Returns `WriteFunctionResult` as IPC bytes.""" + self._check_table_constraint_write(attach_opaque_data, transaction_opaque_data, schema_path, name) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() result = cat.table_update_function_get( @@ -5305,6 +6093,7 @@ def catalog_table_delete_function_get( transaction_opaque_data: bytes | None = None, ) -> bytes: """Get the delete function for a table. Returns `WriteFunctionResult` as IPC bytes.""" + self._check_table_constraint_write(attach_opaque_data, transaction_opaque_data, schema_path, name) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() result = cat.table_delete_function_get( @@ -5325,6 +6114,7 @@ def catalog_table_comment_set( transaction_opaque_data: bytes | None = None, ) -> None: """Set or clear the comment on a table.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "set table comment") self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_comment_set( @@ -5341,12 +6131,17 @@ def catalog_table_column_comment_set( attach_opaque_data: bytes, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], comment: str | None = None, ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: """Set or clear the comment on a table column.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "set column comment") + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_COMMENT, "catalog_table_column_comment_set", transaction_opaque_data + ) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_column_comment_set( @@ -5354,7 +6149,7 @@ def catalog_table_column_comment_set( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, comment=comment, ignore_not_found=ignore_not_found, ) @@ -5369,6 +6164,8 @@ def catalog_table_rename( transaction_opaque_data: bytes | None = None, ) -> None: """Rename a table.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "rename table") + self._check_table_constraint_mutation(attach_opaque_data, transaction_opaque_data, schema_path, name) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_rename( @@ -5385,12 +6182,23 @@ def catalog_table_column_add( attach_opaque_data: bytes, schema_path: list[str], name: str, + column_path: list[str], column_definition: bytes, ignore_not_found: bool = False, if_column_not_exists: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: """Add a new column to a table.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "add column") + self._check_table_constraint_mutation(attach_opaque_data, transaction_opaque_data, schema_path, name) + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_ADD, "catalog_table_column_add", transaction_opaque_data + ) + field = decode_column_definition(column_definition, column_path, type_only=False) + self._validate_column_definition_expressions( + attach_opaque_data, transaction_opaque_data, pa.schema([field]), schema_path, "add column expressions" + ) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_column_add( @@ -5398,6 +6206,7 @@ def catalog_table_column_add( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, + column_path=column_path, column_definition=SerializedSchema(column_definition), ignore_not_found=ignore_not_found, if_column_not_exists=if_column_not_exists, @@ -5408,13 +6217,19 @@ def catalog_table_column_drop( attach_opaque_data: bytes, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, if_column_exists: bool = False, cascade: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: """Drop a column from a table.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "drop column") + self._check_table_constraint_mutation(attach_opaque_data, transaction_opaque_data, schema_path, name) + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_DROP, "catalog_table_column_drop", transaction_opaque_data + ) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_column_drop( @@ -5422,7 +6237,7 @@ def catalog_table_column_drop( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, ignore_not_found=ignore_not_found, if_column_exists=if_column_exists, cascade=cascade, @@ -5433,12 +6248,19 @@ def catalog_table_column_rename( attach_opaque_data: bytes, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], new_column_name: str, ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: """Rename a column.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "rename column") + self._check_table_constraint_mutation(attach_opaque_data, transaction_opaque_data, schema_path, name) + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_RENAME, "catalog_table_column_rename", transaction_opaque_data + ) + column_path_key([new_column_name]) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_column_rename( @@ -5446,7 +6268,7 @@ def catalog_table_column_rename( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, new_column_name=new_column_name, ignore_not_found=ignore_not_found, ) @@ -5456,12 +6278,19 @@ def catalog_table_column_default_set( attach_opaque_data: bytes, schema_path: list[str], name: str, - column_name: str, - expression: str, + column_path: list[str], + expression: bytes, ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: - """Set the default value expression for a column.""" + """Set the default using its mandatory owned canonical expression.""" + definition = self._column_expression(expression, schema_path) + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "set column default") + self._check_table_constraint_mutation(attach_opaque_data, transaction_opaque_data, schema_path, name) + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_DEFAULT, "catalog_table_column_default_set", transaction_opaque_data + ) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_column_default_set( @@ -5469,8 +6298,8 @@ def catalog_table_column_default_set( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, - column_name=column_name, - expression=SqlExpression(expression), + column_path=column_path, + expression=definition, ignore_not_found=ignore_not_found, ) @@ -5479,11 +6308,17 @@ def catalog_table_column_default_drop( attach_opaque_data: bytes, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: """Remove the default value from a column.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "drop column default") + self._check_table_constraint_mutation(attach_opaque_data, transaction_opaque_data, schema_path, name) + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_DEFAULT, "catalog_table_column_default_drop", transaction_opaque_data + ) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_column_default_drop( @@ -5491,7 +6326,7 @@ def catalog_table_column_default_drop( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, ignore_not_found=ignore_not_found, ) @@ -5500,12 +6335,21 @@ def catalog_table_column_type_change( attach_opaque_data: bytes, schema_path: list[str], name: str, + column_path: list[str], column_definition: bytes, - expression: str | None = None, + expression: bytes | None = None, ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: """Change the type of a column.""" + definition = self._column_expression(expression, schema_path) if expression is not None else None + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "change column type") + self._check_table_constraint_mutation(attach_opaque_data, transaction_opaque_data, schema_path, name) + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_TYPE, "catalog_table_column_type_change", transaction_opaque_data + ) + decode_column_definition(column_definition, column_path, type_only=True) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_column_type_change( @@ -5513,8 +6357,9 @@ def catalog_table_column_type_change( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, + column_path=column_path, column_definition=SerializedSchema(column_definition), - expression=SqlExpression(expression) if expression else None, + expression=definition, ignore_not_found=ignore_not_found, ) @@ -5523,11 +6368,18 @@ def catalog_table_not_null_drop( attach_opaque_data: bytes, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: """Remove NOT NULL constraint from a column.""" + self._require_catalog_capability( + attach_opaque_data, CONSTRAINT_MUTATE, "catalog_table_not_null_drop", transaction_opaque_data + ) + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_NULLABILITY, "catalog_table_not_null_drop", transaction_opaque_data + ) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_not_null_drop( @@ -5535,7 +6387,7 @@ def catalog_table_not_null_drop( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, ignore_not_found=ignore_not_found, ) @@ -5544,11 +6396,19 @@ def catalog_table_not_null_set( attach_opaque_data: bytes, schema_path: list[str], name: str, - column_name: str, + column_path: list[str], ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: """Add NOT NULL constraint to a column.""" + self._require_catalog_capability( + attach_opaque_data, CONSTRAINT_MUTATE, "catalog_table_not_null_set", transaction_opaque_data + ) + self._require_catalog_capability(attach_opaque_data, CONSTRAINT_ENFORCE, "catalog_table_not_null_set") + if len(column_path_key(column_path)) > 1: + self._require_catalog_capability( + attach_opaque_data, COLUMN_NESTED_NULLABILITY, "catalog_table_not_null_set", transaction_opaque_data + ) self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_table_name=name) cat = self._get_catalog() cat.table_not_null_set( @@ -5556,7 +6416,7 @@ def catalog_table_not_null_set( transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, name=name, - column_name=column_name, + column_path=column_path, ignore_not_found=ignore_not_found, ) @@ -5580,6 +6440,8 @@ def catalog_view_get( schema_path=schema_path, name=name, ) + if info is not None: + info = self._view_for_capabilities(info, self._effective_catalog_capabilities(attach_opaque_data)) return ViewsResponse.from_optional(info) def catalog_view_create( @@ -5590,9 +6452,27 @@ def catalog_view_create( definition: str, on_conflict: OnConflict, transaction_opaque_data: bytes | None = None, + aliases: list[str] | None = None, + security: str = "regular", + comment: str | None = None, + tags: dict[str, str] | None = None, + column_comments: dict[str, str] | None = None, ) -> None: """Create a new view.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "create view") self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_view_name=name) + aliases = [] if aliases is None else aliases + ViewInfo.validate_aliases(aliases) + ViewInfo.validate_security(security) + ViewInfo.validate_create_metadata(comment, tags, column_comments) + if security == "barrier": + self._require_catalog_capability( + attach_opaque_data, VIEW_BARRIER_CREATE, "catalog_view_create", transaction_opaque_data + ) + if on_conflict == OnConflict.REPLACE: + self._check_existing_view_security( + attach_opaque_data, transaction_opaque_data, schema_path, name, replace=True + ) cat = self._get_catalog() cat.view_create( attach_opaque_data=self._unwrap_attach(attach_opaque_data), @@ -5600,6 +6480,11 @@ def catalog_view_create( schema_path=schema_path, name=name, definition=definition, + aliases=list(aliases), + security=security, + comment=comment, + tags=dict(tags or {}), + column_comments=dict(column_comments or {}), on_conflict=on_conflict, ) @@ -5613,7 +6498,9 @@ def catalog_view_drop( transaction_opaque_data: bytes | None = None, ) -> None: """Drop a view.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "drop view") self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_view_name=name) + self._check_existing_view_security(attach_opaque_data, transaction_opaque_data, schema_path, name) cat = self._get_catalog() cat.view_drop( attach_opaque_data=self._unwrap_attach(attach_opaque_data), @@ -5634,7 +6521,9 @@ def catalog_view_rename( transaction_opaque_data: bytes | None = None, ) -> None: """Rename a view.""" + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "rename view") self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_view_name=name) + self._check_existing_view_security(attach_opaque_data, transaction_opaque_data, schema_path, name) cat = self._get_catalog() cat.view_rename( attach_opaque_data=self._unwrap_attach(attach_opaque_data), @@ -5656,6 +6545,7 @@ def catalog_view_comment_set( ) -> None: """Set or clear the comment on a view.""" self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_view_name=name) + self._check_existing_view_security(attach_opaque_data, transaction_opaque_data, schema_path, name) cat = self._get_catalog() cat.view_comment_set( attach_opaque_data=self._unwrap_attach(attach_opaque_data), @@ -5666,6 +6556,33 @@ def catalog_view_comment_set( ignore_not_found=ignore_not_found, ) + def catalog_view_column_comment_set( + self, + attach_opaque_data: bytes, + schema_path: list[str], + name: str, + column_path: list[str], + comment: str | None = None, + ignore_not_found: bool = False, + transaction_opaque_data: bytes | None = None, + ) -> None: + """Set or clear a view output comment; column_path has one raw component.""" + if len(column_path_key(column_path)) != 1: + raise ValueError("view column comments require one raw output identifier") + if comment is not None and not isinstance(comment, str): + raise ValueError("view column comment must be a string or None") + self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_view_name=name) + self._check_existing_view_security(attach_opaque_data, transaction_opaque_data, schema_path, name) + self._get_catalog().view_column_comment_set( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + schema_path=schema_path, + name=name, + column_path=list(column_path), + comment=comment, + ignore_not_found=ignore_not_found, + ) + # --------------------------------------------------------------------------- # VgiProtocol implementation - Catalog Macros # --------------------------------------------------------------------------- @@ -5686,27 +6603,32 @@ def catalog_macro_get( schema_path=schema_path, name=name, ) + if info is not None: + info = self._macro_for_capabilities(info, self._effective_catalog_capabilities(attach_opaque_data)) return MacrosResponse.from_optional(info) def catalog_macro_create(self, request: MacroCreateRequest) -> None: - """Create a new macro.""" + """Create one mandatory macro family without representation negotiation.""" + request.__post_init__() self._enrich_catalog_span(vgi_schema_path=request.schema_path, vgi_macro_name=request.name) - cat = self._get_catalog() - cat.macro_create( + self._require_dependency_transaction( + request.attach_opaque_data, request.transaction_opaque_data, "create macro family" + ) + selected = self._effective_catalog_capabilities(request.attach_opaque_data) + for member in request.overloads: + for parameter in member.parameters: + if parameter.declared_type is not None: + require_logical_profiles(parameter.declared_type, selected_profiles=selected.read) + if parameter.default_value is not None: + require_logical_values(parameter.default_value, selected_profiles=selected.read) + self._get_catalog().macro_create( attach_opaque_data=self._unwrap_attach(request.attach_opaque_data), - transaction_opaque_data=self._unwrap_transaction( - request.transaction_opaque_data, request.attach_opaque_data - ) - if request.transaction_opaque_data - else None, + transaction_opaque_data=self._unwrap_tx_opt(request.transaction_opaque_data, request.attach_opaque_data), schema_path=request.schema_path, name=request.name, macro_type=request.macro_type, - parameters=request.parameters, - definition=request.definition, + overloads=request.overloads, on_conflict=request.on_conflict, - parameter_default_values=request.parameter_default_values, - arguments_schema=request.arguments_schema, ) def catalog_macro_drop( @@ -5717,10 +6639,10 @@ def catalog_macro_drop( ignore_not_found: bool = False, transaction_opaque_data: bytes | None = None, ) -> None: - """Drop a macro.""" + """Drop the complete family through the existing provider operation.""" self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_macro_name=name) - cat = self._get_catalog() - cat.macro_drop( + self._require_dependency_transaction(attach_opaque_data, transaction_opaque_data, "drop macro family") + self._get_catalog().macro_drop( attach_opaque_data=self._unwrap_attach(attach_opaque_data), transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), schema_path=schema_path, @@ -5744,7 +6666,8 @@ def catalog_schema_contents_macros( path=path, type=type, ) - return MacrosResponse.from_infos(list(infos)) + selected = self._effective_catalog_capabilities(attach_opaque_data) + return MacrosResponse.from_infos([self._macro_for_capabilities(info, selected) for info in infos]) # --------------------------------------------------------------------------- # VgiProtocol implementation - Catalog Indexes @@ -5766,29 +6689,75 @@ def catalog_index_get( schema_path=schema_path, name=name, ) - return IndexesResponse.from_optional(info) + return IndexesResponse.from_optional( + self._index_for_capabilities(info, self._effective_catalog_capabilities(attach_opaque_data)) + if info is not None + else None + ) + + def catalog_index_option_definitions( + self, + attach_opaque_data: bytes, + table: bytes, + algorithm: str, + transaction_opaque_data: bytes | None = None, + ) -> OptionDefinitions: + """Read complete provider option definitions for one exact algorithm.""" + self._require_catalog_capability(attach_opaque_data, INDEX_DEFINITION_READ, "read index options") + ref = CatalogObjectRef.deserialize_from_bytes(table) + if ref.kind != "table" or not algorithm or "\0" in algorithm: + raise ValueError("index options require a table reference and nonempty algorithm") + result = self._get_catalog().index_option_definitions( + attach_opaque_data=self._unwrap_attach(attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt(transaction_opaque_data, attach_opaque_data), + table=ref, + algorithm=algorithm, + ) + result.__post_init__() + require_logical_profiles( + result.schema, selected_profiles=self._effective_catalog_capabilities(attach_opaque_data).read + ) + return result def catalog_index_create(self, request: IndexCreateRequest) -> None: - """Create a new index.""" + """Require the negotiated contract before an atomic provider mutation.""" + request.__post_init__() self._enrich_catalog_span(vgi_schema_path=request.schema_path, vgi_index_name=request.name) - cat = self._get_catalog() - cat.index_create( - attach_opaque_data=self._unwrap_attach(request.attach_opaque_data), - transaction_opaque_data=self._unwrap_transaction( - request.transaction_opaque_data, request.attach_opaque_data + self._require_catalog_capability( + request.attach_opaque_data, INDEX_DEFINITION_CREATE, "create typed index", request.transaction_opaque_data + ) + self._require_index_definition_profiles(request.attach_opaque_data, request.definition) + if request.on_conflict == OnConflict.REPLACE: + self._require_catalog_capability( + request.attach_opaque_data, INDEX_DROP, "replace typed index", request.transaction_opaque_data ) - if request.transaction_opaque_data - else None, + self._check_index_drop_association( + request.attach_opaque_data, request.transaction_opaque_data, request.schema_path, request.name + ) + self._get_catalog().index_create( + attach_opaque_data=self._unwrap_attach(request.attach_opaque_data), + transaction_opaque_data=self._unwrap_tx_opt(request.transaction_opaque_data, request.attach_opaque_data), schema_path=request.schema_path, name=request.name, - table_name=request.table_name, - index_type=request.index_type, - constraint_type=request.constraint_type, - expressions=list(request.expressions), + definition=request.definition, on_conflict=request.on_conflict, - options=dict(request.options) if request.options else None, ) + def _check_index_drop_association( + self, attach: bytes, transaction: bytes | None, path: list[str], name: str + ) -> None: + """Gate associated-index mutations; providers authorize their atomic closure.""" + info = self._get_catalog().index_get( + attach_opaque_data=self._unwrap_attach(attach), + transaction_opaque_data=self._unwrap_tx_opt(transaction, attach), + schema_path=path, + name=name, + ) + if info is not None: + self._index_for_capabilities(info, self._effective_catalog_capabilities(attach)) + if info.definition.associated_constraint is not None: + self._require_catalog_capability(attach, CONSTRAINT_MUTATE, "mutate associated index", transaction) + def catalog_index_drop( self, attach_opaque_data: bytes, @@ -5800,6 +6769,8 @@ def catalog_index_drop( ) -> None: """Drop an index.""" self._enrich_catalog_span(vgi_schema_path=schema_path, vgi_index_name=name) + self._require_catalog_capability(attach_opaque_data, INDEX_DROP, "drop typed index", transaction_opaque_data) + self._check_index_drop_association(attach_opaque_data, transaction_opaque_data, schema_path, name) cat = self._get_catalog() cat.index_drop( attach_opaque_data=self._unwrap_attach(attach_opaque_data), @@ -5825,7 +6796,8 @@ def catalog_schema_contents_indexes( path=path, type=SchemaObjectType.INDEX, ) - return IndexesResponse.from_infos(list(infos)) + selected = self._effective_catalog_capabilities(attach_opaque_data) + return IndexesResponse.from_infos([self._index_for_capabilities(info, selected) for info in infos]) # --------------------------------------------------------------------------- # Lifecycle